ukiyuの思考研究所。

人生道半ばでの知識の棚卸し、次のお仕事決まるまで。オブジェクト指向モデリングとかに関してつらつらと。ご意見、ご感想等は気楽にどうぞ。。。

四角を動かすだけ(3)。

mdl.hatenablog.com

前回↑の続き。

package com.ukiyu.blog004;

public class Service {

	public static void main(String[] args) {
		System.out.println("Ver.004 Start");

		// 座標(100, 100)に存在する20x30の大きさの四角形
		Rectangle rectangle = Rectangle.of(100, 100, 20, 30);
		System.out.println(rectangle);

		// x方向に10, y方向に20動かす。
		Rectangle addedRectangle = rectangle.addXY(10, 20);

		System.out.println(addedRectangle);
	}
}
package com.ukiyu.blog004;

public class Rectangle {
	private final double x;
	private final double y;
	private final double height;
	private final double width;

	private Rectangle(double x, double y, double height, double width) {
		this.x = x;
		this.y = y;
		this.height = height;
		this.width = width;
	}

	static public Rectangle of(double x, double y, double height, double width) {
		return new Rectangle(x, y, height, width);
	}

	public Rectangle addXY(double x, double y) {
		return of(this.x + x, this.y + y, this.height, this.width);
	}

	@Override
	public String toString() {
		return "Rectangle (x, y) = ( " + x + ", " + y + ") , (height, width) = ( " + height + ", " + width + ")";
	}
}

コードはこちら↓。
https://github.com/ukiyu/blog/tree/master/ForBlog/src/com/ukiyu/blog004

引数が多い。

static ファクトリメソッドである Rectangle.of() の引数が多くて何を示しているのか、コメント無しではよく分かりません。
コードがよく分からないからコメントを残そう、と思った時は、まずそのコメントをコードで表せないか、を検討すべきです。
また、常に同時に扱われるべきものは、1つのクラスとしてモデル化すべきでしょう。
ここでは、x, yという位置を表すものとheight, witdhという大きさを表すものをそれぞれクラスとします。

public class Position {
	private final double x;
	private final double y;

	private Position(double x, double y) {
		this.x = x;
		this.y = y;
	}

	static public Position of(double x, double y) {
		return new Position(x, y);
	}

	@Override
	public String toString() {
		return "(x, y) = (" + x + ", " + y + ")";
	}
}
public class Size {
	private final double height;
	private final double width;

	private Size(double height, double width) {
		this.height = height;
		this.width = width;
	}

	static public Size of(double height, double width) {
		return new Size(height, width);
	}

	@Override
	public String toString() {
		return "(height, width) = (" + height + ", " + width + ")";
	}
}

これらの新しく用意したクラスを用いると、

public class Service {

	public static void main(String[] args) {
		System.out.println("Ver.005 Start");

		// 座標(100, 100)
		Position position = Position.of(100, 100);
		// 大きさ(20, 30)
		Size size = Size.of(20, 30);
		// 四角形
		Rectangle rectangle = Rectangle.of(position, size);
		System.out.println(rectangle);

		// x方向に10, y方向に20動かす。
		Rectangle addedRectangle = rectangle.addXY(10, 20);

		System.out.println(addedRectangle);
	}
}

となりますが、ここで addXY() の実装に関して少し考えます。

public class Rectangle {
	private final Position position;
	private final Size size;

	private Rectangle(Position position, Size size) {
		this.position = position;
		this.size = size;
	}

	static public Rectangle of(Position position, Size size) {
		return new Rectangle(position, size);
	}

	public Rectangle addXY(double x, double y) {
		//return of(this.x + x, this.y + y, this.height, this.width);
		return null; // どうしよう・・・。
	}

	@Override
	public String toString() {
		return "Rectangle " + position + ", " + size;
	}
}

既存の実装をそのまま再現するのであれば、Position及びSizeクラスにgetterを追加して

	public Rectangle addXY(double x, double y) {
		return of(Position.of(position.getX() + x, position.getY() + y),
				Size.of(size.getHeight(), size.getWidth()));
	}

となります。

だから、get()するなと・・・。

前述した通りget()には弊害があるので、get()せずに目的を達成することを検討します。
ここでも求めるな、命じよ。の原則を思い出します。
求めずに(getせずに)命じる、とはここでは何を意味をするのか?
positionに対して行っていることは足し算です。
であれば、それをpositionに命じるだけのことですね。

// class Position
	public Position plus(double x, double y) {
		return of(this.x + x, this.y + y);
	}

如何でしょうか。このようにそのままdoubleを引数に取ることも可能なのですが、
ここは少し気を利かせて

// class Position
	public Position plus(Position other) {
		return of(x + other.x, y + other.y);
	}

とします。これを閉じた操作と呼びます。
Positionクラスのメソッドであるplus()はPosition型以外に依存していないのです。
すなわち、他クラスの変更の影響を全く受けない素敵なメソッドなのです。

さて、sizeに対してはどうでしょうか。get()しているだけで特に他のことはしていません。
であれば、static ファクトリメソッドの Size.of()を呼ばずに、sizeを直接引数として渡すことで
get()すること自体は避けることが出来ます。
これの意味するところは、ここで新たに生成されるRectangleインスタンスと従来のRectangleインスタンスがSizeインスタンスを共有するということです。
これは安全と言えるでしょうか?

答えはYesです。

とあるインスタンスを共有することが危ない場合というのは
その共有されているインスタンスの状態が可変な時です。
Sizeインスタンス不変です。いくら共有されても何の問題も生じませんし、
どんなアクセスをされても不整合も生じないのです。

addXY()の名前はplus()に変更します。
add()という名前はJavaのコレクションクラスのadd()メソッドを連想させ、これはインスタンスの状態を変更するものだからです。

// class Rectangle
	public Rectangle plus(Position other) {
		return of(position.plus(other), size);
	}

	public Rectangle plus(Size other) {
		return of(position, size.plus(other));
	}

YAGNIの原則に反しますが、Sizeも足せるようにしてみました。
型を適切に定義することで、オーバーロードが効果的に行えることが分かります。
doubleのままではメソッド名を別々に付けてあげる必要があり冗長です。
ちなみに、YAGNI とは "You ain't gonna need it." の頭文字をとったもので、
"あなたはそれを必要としない" ということです。現状Sizeに関する足し算は求められていないです。

修正後。

package com.ukiyu.blog006;

public class Service {

	public static void main(String[] args) {
		System.out.println("Ver.006 Start");

		Position position = Position.of(100, 100);
		Size size = Size.of(20, 30);
		Rectangle rectangle = Rectangle.of(position, size);
		System.out.println(rectangle);

		Position distance = Position.of(10, 20);
		Rectangle movedRactangle = rectangle.plus(distance);

		System.out.println(movedRactangle);
	}
}
package com.ukiyu.blog006;

public class Rectangle {
	private final Position position;
	private final Size size;

	private Rectangle(Position position, Size size) {
		this.position = position;
		this.size = size;
	}

	static public Rectangle of(Position position, Size size) {
		return new Rectangle(position, size);
	}

	public Rectangle plus(Position other) {
		return of(position.plus(other), size);
	}

	public Rectangle plus(Size other) {
		return of(position, size.plus(other));
	}

	@Override
	public String toString() {
		return "Rectangle " + position + ", " + size;
	}
}
package com.ukiyu.blog006;

public class Position {
	private final double x;
	private final double y;

	private Position(double x, double y) {
		this.x = x;
		this.y = y;
	}

	static public Position of(double x, double y) {
		return new Position(x, y);
	}

	public Position plus(Position other) {
		return of(x + other.x, y + other.y);
	}

	@Override
	public String toString() {
		return "(x, y) = (" + x + ", " + y + ")";
	}
}
package com.ukiyu.blog006;

public class Size {
	private final double height;
	private final double width;

	private Size(double height, double width) {
		this.height = height;
		this.width = width;
	}

	static public Size of(double height, double width) {
		return new Size(height, width);
	}

	public Size plus(Size other) {
		return of(height + other.height, width + other.width);
	}

	@Override
	public String toString() {
		return "(height, width) = (" + height + ", " + width + ")";
	}
}

コンソール出力。

Ver.006 Start
Rectangle (x, y) = (100.0, 100.0), (height, width) = (20.0, 30.0)
Rectangle (x, y) = (110.0, 120.0), (height, width) = (20.0, 30.0)

コードはこちら↓。
https://github.com/ukiyu/blog/tree/master/ForBlog/src/com/ukiyu/blog006

コメントに書かれてあったことはコードで表現できるようになったのでコメントは消しました。
修正はこれで完了です。
今回は駄目なモデルからリファクタリングを行う形をとりましたが、
リファクタリングという遠回りをしなくても、この程度のことであれば最初から適切にモデリングを行いたいところです。

次からは自由気ままに適当な機能を追加していこうかな、と思っています。

続く。