Value Object Design Pattern

Einführung

Mit Value Object werden einfache Objekte bezeichnet, bei denen die Werte der Eigenschaften bei einem Vergleich verwendet werden. Es werden also nicht, wie sonst bei Referenztypen üblich, die Identitäten der Objekte verglichen.

C# bietet für dieses Pattern die Möglichkeit  ein Struct zu definieren. Structs sind bereits per Definition Werttypen. Beim Vergleich von Structs werden also die Werte miteinander verglichen. Es bedarf nur kleinen Anpassungen damit es vollends dem Pattern entspricht.

in JAVA existiert hingegen keine Möglichkeit einen benutzerdefinierten Value Type zu erstellen. Früher kam es jedoch vor, dass Data Transfer Objects als Value Objects bezeichnet wurden.

C#

        static void Main(string[] args)
        {
            var catFromStructA = new CatStruct { Name = "Kitty", Color = "Black" };
            var catFromStructB = new CatStruct { Name = "Kitty", Color = "Black" };
            var resultStructs = catFromStructA.Equals(catFromStructB);

            Console.WriteLine($"Struct equation: {resultStructs}");


            var catFromClassA = new CatClass { Name = "Kitty", Color = "Black" };
            var catFromClassB = new CatClass { Name = "Kitty", Color = "Black" };
            var resultClasses = catFromClassA.Equals(catFromClassB);

            Console.WriteLine($"Class equation: {resultClasses}");

            Console.ReadLine();
        }

        public struct CatStruct
        {
            public string Name;
            public string Color;
        }

        public class CatClass
        {
            public string Name;
            public string Color;
        }


Die Ausgabe:

Struct equation: True
Class equation: False

Beim Beispiel erkennt man schnell den Unterschied beim Vergleich von Klassen und Structs. Allen Instanzen wurden dieselben Werte zugewiesen, doch nur beim Vergleich der Structs wird TRUE zurückgegeben.

JAVA

public class TestClass {

	public static void main(String[] args) {
		Cat CatFromClassA = new Cat() {{ name = "Kitty"; color = "Black"; }};
		Cat CatFromClassB = new Cat() {{ name = "Kitty"; color = "Black"; }};	
		boolean resultClasses = CatFromClassA.equals(CatFromClassB);
		
		System.out.println("Class equation: " + resultClasses);
	}
}

public class Cat {
	public String name;
	public String color;
}

Die Ausgabe:

Class equation: false

Im diesem Beispiel wird ein Vergleich von zwei Klassen mithilfe der equals() Methode gezeigt, der in einem FALSE resultiert. Die Identitäten der Objekte unterscheiden sich da sie seperat erstellt wurden.

Nun kann es aus vielerlei Gründen vorkommen, dass kein Struct verwendet werden kann (Ableitung, EntityFramework, etc.) oder, wie im Falle von JAVA, keine solche Möglichkeit besteht. Für diesen Zweck kann das Value Object Design Pattern angewendet werden. Das Ziel ist es, wie bei einem Struct, einen Wertevergleich statt eines Identitätsvergleiches zu erreichen.

Anwendung des Pattern

Im folgenden Beispiel sollen zwei Rechtecke miteinander verglichen werden. Es werden zwei Instanzen der Klasse Rectangle erstellt und die Eigenschaften initialisiert. Anschließend erfolgt ein Vergleich und die Ausgabe des Ergebnisses:

C#

        static void Main(string[] args)
        {
            var rectangleA = new Rectangle { Height = 25, Width = 2 };
            var rectangleB = new Rectangle { Height = 25, Width = 2 };

            var result = rectangleA.Equals(rectangleB);

            Console.WriteLine($"Equation resulted in {result}");
            Console.ReadLine();
        }

        public class Rectangle
        {
            public int Width;
            public int Height;
        }

JAVA

public class TestClass {

	public static void main(String[] args) 	{
		Rectangle RectangleA = new Rectangle() {{ height = 25; width = 2; }};
		Rectangle RectangleB = new Rectangle() {{ height = 25; width = 2; }};
		
		boolean result = RectangleA.equals(RectangleB);
		
		System.out.println("Equation resulted in: " + result);
	}
}

public class Rectangle {
	public int width;
	public int height;
}

Werden die beiden Rechtecke miteinander verglichen erhält man, wie erwartet, FALSE.
Im nächsten Schritt soll das Value Object Pattern angewandt werden um bei einem Vergleich, statt der Identität, die Abmessungen zu vergleichen. Um das zu erreichen wird die Methode equals() der Rectangle Klasse überschrieben:

C#

        public class Rectangle
        {
            public int Width;
            public int Height;

            public override bool Equals(object obj)
            {
                var equationRectangle = (Rectangle)obj;
                var result = equationRectangle.Width == Width 
                    && equationRectangle.Height == Height;

                return result;
            }
        }

JAVA

public class Rectangle {
	public int width;
	public int height;
	
	public boolean equals(Object obj) {
		Rectangle equationRectangle = (Rectangle)obj;
		boolean result = equationRectangle.width == width 
			&& equationRectangle.height == height;

		return result;
	}
}

Nun werden bei einem Vergleich mit equals() die Werte für Width und Height verglichen und man erhält, korrekter Weise, ein TRUE beim obigen Vergleich.

Wird nun jedoch ein Objekt eines anderen Types mit Rectangle verglichen, bricht das Programm mit einer Ausnahme ab. In C# ist es die InvalidCastException und in JAVA die ClassCastException. Die überladene equals Method muss also zunächst auf einen vergleichbaren Typ prüfen und wird folgendermaßen erweitert:

C#

            public override bool Equals(object obj)
            {
                if (obj is Rectangle)
                {
                    var equationRectangle = (Rectangle)obj;
                    var result = equationRectangle.Width == Width 
                        && equationRectangle.Height == Height;

                    return result;
                }
                else
                    return false;
            }

JAVA

	public boolean equals(Object obj) {
		if(obj instanceof Rectangle) {
			Rectangle equationRectangle = (Rectangle)obj;
			boolean result = equationRectangle.width == width 
				&& equationRectangle.height == height;
			
			return result;
		}
		else
			return false;
	}

Wird nun ein Objekt anderen Types geprüft, wird in den else-Abschnitt gesprungen und ein FALSE zurück gegeben.

Zusätzlich, zu der methode equals(), kann auch ein Operator verwendet werden, um zwei Objekte miteinander zu vergleichen.Wird also die Zeile

var result = rectangleA.Equals(rectangleB);
in
var result = rectangleA == rectangleB;

geändert, erhält man wieder ein FALSE da in diesem Fall erneut die Identitäten miteinander verglichen werden. In C# hat man die Möglichkeit, durch eine Operatoren Üerladung, festzulegen, was bei der Überprüfung verglichen wird. Die Klasse Rectangle kann also mit eigenen Operatoren ausgestattet werden um diesen Vergleich auf die Werte zu beziehen. JAVA bietet diese Möglichkeit nicht! Die Klasse wird im C# Code nun um Folgendes erweitert:

C#

            public static bool operator ==(Rectangle RecA, Rectangle RecB)
            {
                if ((object)RecA == null || (object)RecB == null)
                    return false;

                var result = RecA.Width == RecB.Width && RecA.Height == RecB.Height;
                return result;
            }

            public static bool operator !=(Rectangle RecA, Rectangle RecB)
            {
                if ((object)RecA == null || (object)RecB == null)
                    return false;

                var result = RecA.Width != RecB.Width || RecA.Height != RecB.Height;
                return result;
            }

Vor dem Vergleich wird zunächst auf einen NULL-Wert geprüft, da in C# jedes Objekt auch den Wert NULL annehmen kann. Würde dieser Schritt nicht getan, würde eine NullReferenceException ausgelöst werden. Für diesen Vergleich müssen die übergebenen Rectangles in den Object-Type umgewandelt werden da ansonsten der == Operator erneut den == Operator aufrufen würde und das so lange bis eine StackOverflowException ausgelöst wird.

Beim überladenen Vergleichsopertor wird nun, genauso wie bei equals(), ein TRUE zurück gegeben. Zusammen mit == sollte immer auch der != Operator überladen werden. Damit nun auch auf eine Ungleichheit der Werte geprüft werden.

Value-Objects sollen unveränderbar sein

Die Werte eines Value-Objectes sollen, per Definition, nach der Initialisierung nicht mehr verändert werden können. Zur Änderung wird daher eine Neuinstanzierung der Classe notwendig. Diese Regel wird vom Struct-Typ in C# übrigens nicht eingehalten und ohne eine Änderung am Beispielstruct aus dem Codesample1 entspricht es nicht dem Value-Object Pattern.

Zurück zum Rectangle Beispiel: Die Sichtbarkeit der Eigenschaften wird auf private gesenkt. In C# besteht zusätzlich die Möglichkeit, sie als readonly zu deklarieren, was eine Wertänderung nach der Initialisierung verhindert. Anschließend wird ein Konstruktor erstellt, der die benötigten Parameter enthält um Zuweisung während der Initialisierung zu ermöglichen:

C#

    class Program
    {
        static void Main(string[] args)
        {
            var RectangleA = new Rectangle(2, 25);
            var RectangleB = new Rectangle(2, 25);

            var resultEquals = RectangleA.Equals(RectangleB);

            Console.WriteLine($"Equation results in {resultEquals}");
            Console.ReadLine();
        }

        public class Rectangle
        {
            private readonly int Width;
            private readonly int Height;

            public Rectangle(int width, int height)
            {
                Width = width;
                Height = height;
            }

            public override bool Equals(object obj)
            {
                if (obj is Rectangle)
                {
                    var equationRectangle = (Rectangle)obj;
                    var result = equationRectangle.Width == Width 
                        && equationRectangle.Height == Height;

                    return result;
                }
                else
                    return false;
            }
        }
    }

Anmerkung: Die Operatorenüberladung wurde der Übersicht halber wieder entfernt.

JAVA

public class TestClass {

	public static void main(String[] args) {
		Rectangle RectangleA = new Rectangle(2, 25);
		Rectangle RectangleB = new Rectangle(2, 25);
		
		boolean result = RectangleA.equals(RectangleB);
		
		System.out.println("Equation resulted in: " + result);
	}
}

public class Rectangle {
	private int width;
	private int height;
	
	public Rectangle(int width, int height) {
		this.width = width;
		this.height = height;
	}
	
	public boolean equals(Object obj) {
		if(obj instanceof Rectangle) {
			Rectangle equationRectangle = (Rectangle)obj;
			boolean result = equationRectangle.width == width 
					&& equationRectangle.height == height;
			
			return result;
		}
		else
			return false;
	}
}

Damit wäre das Ende dieses Beitrags erreicht. Zusammenfassend kann über das Value-Object Pattern gesagt werden:

  • Bei Vergleichen werden die Eigenschaftswerte der Objekte statt der Identität verglichen
  • Um dies zu erreichen wird die Metode equals() überschrieben
  • C#: Zusätzlich zu equals() werden die Vergleichsoperatoren == und != überschrieben
  • Die Werte sind, nach der Initialisierung, unveränderlich
  • C#: Structs entsprechen dem Pattern wenn ihre Eigenschaften readonly sind

Vielen Dank fürs Lesen 🙂

Leave a Reply

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>