ukiyuの思考研究所。

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

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

mdl.hatenablog.com

前回↑の続き。

さて、次の機能追加は大きさを変更する際の起点を指定できるようにしてみたいと思います。前回、大きさを変更できるようにしましたが、Potisionが起点となっています。左上が(0, 0)のxy座標系とすると、四角形の左上の角の位置は変わらずに右下の角が動く感じです。

f:id:ukiyu:20191004182516p:plain
左上が起点。

大きさの変更は四角の中心を起点に4つの角がそれぞれ移動する、としても妥当な動作かと思います。
折角なので、左上、上、右上、右、・・・と9つの点を起点として選択できるようにしてみます。

f:id:ukiyu:20191004182543p:plain
中心が起点。

例えば中心が起点とすると、四角のPositionが移動することになり、その移動量は変更する大きさに依存します。これらの計算はそれだけで一つの責務と考え、クラスを新たに定義します。四角形の場合は9つの起点が選択できるとして、新しいクラスはRectangleの内部クラスとします。

// class Rectangle
	static public class Scaling {
		private final Size.Rate rate;

		private Scaling(Size.Rate rate) {
			this.rate = rate;
		}

		static public Scaling of(Size.Rate rate) {
			return new Scaling(rate);
		}

		public Rectangle apply(Position position, Size size) {
			Size multipliedSize = size.multiply(rate);
			return Rectangle.of(/* 位置の計算が必要 */, multipliedSize);
		}
	}

さて、位置の計算の仕方は起点となる9つでそれぞれ異なります。これを選択してあげる必要があるのですが

		switch(base) {
		case TOP_LEFT:
			// 左上が起点の時の処理。
			break;
		case TOP:
			// 上が起点の時の処理。
			break;
		case TOP_RIGHT:
			// 右上が起点の時の処理。
			break;
		case LEFT:
			// 左が起点の時の処理。
			break;
		/* 残りの5つは略 */
		default:
			// ここに来たらどうしよう。
			break;
		}

のようなswitch文は書きたくないのです。依然としてこのようなswitch文を現場では見かけるのですが、複雑になることは目に見えているので、最初からストラテジー化することをお勧めします。
caseに来る何かしらのパラメータを振る舞いを持つオブジェクトとします。

		TOP_LEFT.do(){ 左上が起点の時の処理(); }
		TOP.do(){ 上が起点の時の処理(); }

みたいなことを実現できれば、上述のswitch文は

		base.do();

とシンプルになり、defaultに来たらどうしよう、という悩みもなくなります。
C/C++では、パラメータはenumで定義されることが多く、結果としてswitch文が乱立することになるので出来る限りenumを使わずにクラスで表現することを検討すべきですが、Javaではenumはクラスと同様に振る舞いを持つことが出来るので、ここではenumを使って実装してみます。

// class Rectangle
	static public class Scaling {
		public enum Base{
			TOP_LEFT(Size.Rate.ZERO),
			TOP(Size.Rate.of(0, 0.5)),
			TOP_RIGHT(Size.Rate.of(0, 1)),
			LEFT(Size.Rate.of(0.5, 0)),
			CENTER(Size.Rate.HALF),
			RIGHT(Size.Rate.of(0.5, 1)),
			BOTTOM_LEFT(Size.Rate.of(1, 0)),
			BOTTOM(Size.Rate.of(1, 0.5)),
			BOTTOM_RIGHT(Size.Rate.SAME);

			private final Size.Rate rate;

			private Base(Size.Rate rate){
				this.rate = rate;
			}

			private Position adjust(Position current, Size difference) {
				return current.minus(difference.multiply(rate).toPosition());
			}
		}

		private final Size.Rate rate;
		private final Base base;

		private Scaling(Size.Rate rate, Base base) {
			this.rate = rate;
			this.base = base;
		}

		static public Scaling of(Size.Rate rate, Base base) {
			return new Scaling(rate, base);
		}

		static public Scaling of(Size.Rate rate) {
			return new Scaling(rate, Base.TOP_LEFT);
		}

		public Rectangle apply(Position position, Size size) {
			Size multipliedSize = size.multiply(rate);
			Size differenceSize = multipliedSize.minus(size);

			return Rectangle.of(base.adjust(position, differenceSize), multipliedSize);
		}
	}
// class Size
	public Position toPosition(Position offset) {
		return Position.of(height, width).plus(offset);
	}

	public Position toPosition() {
		return toPosition(Position.of(0, 0));
	}

	static public class Rate {
		private final double x;
		private final double y;

		static public final Rate ZERO = of(0);
		static public final Rate HALF = of(0.5);
		static public final Rate SAME = of(1);
		static public final Rate DOUBLE = of(2);

計算に必要なパラメータをenum Baseに予め持たせておいて、実際に実行する際にその時さらに必要なパラメータを渡し、計算するように命じます。計算する際に、SizeとPositionの演算が必要になったので今回はSizeをPositionに変換するメソッドを追加しています。どちらがどちらに依存するのが自然であるか、はケースバイケースなのでよく検討する必要があります。

また、static public final Rateで定義されたパラメータを追加しました。よく使うものは最初から定義しておいてすぐに使えるようにしておくことで、毎回のコンストラクタの呼び出しが削減できます。また、これが安全であるのもオブジェクトが不変であり、共有して使用することが可能だからです。

機能追加後。

package com.ukiyu.blog009;

import com.ukiyu.blog009.Rectangle.Scaling;

public class Service {

	public static void main(String[] args) {
		System.out.println("Ver.009 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);

		Size.Rate rate = Size.Rate.of(1.5);
		Rectangle resizedRactangle = rectangle.resize(rate);

		System.out.println(resizedRactangle);

		System.out.println("");
		Rectangle resizedTop = rectangle.resize(Scaling.of(rate, Scaling.Base.TOP));
		System.out.println(resizedTop);

		Rectangle resizedTopRight = rectangle.resize(Scaling.of(rate, Scaling.Base.TOP_RIGHT));
		System.out.println(resizedTopRight);


		Rectangle resizedLeft = rectangle.resize(Scaling.of(rate, Scaling.Base.LEFT));
		System.out.println(resizedLeft);

		Rectangle resizedCenter = rectangle.resize(Scaling.of(rate, Scaling.Base.CENTER));
		System.out.println(resizedCenter);

		Rectangle resizedRight = rectangle.resize(Scaling.of(rate, Scaling.Base.RIGHT));
		System.out.println(resizedRight);


		Rectangle resizedBottomLeft = rectangle.resize(Scaling.of(rate, Scaling.Base.BOTTOM_LEFT));
		System.out.println(resizedBottomLeft);

		Rectangle resizedBottom = rectangle.resize(Scaling.of(rate, Scaling.Base.BOTTOM));
		System.out.println(resizedBottom);

		Rectangle resizedBottomRight = rectangle.resize(Scaling.of(rate, Scaling.Base.BOTTOM_RIGHT));
		System.out.println(resizedBottomRight);
	}
}
package com.ukiyu.blog009;

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));
	}

	static public class Scaling {
		public enum Base{
			TOP_LEFT(Size.Rate.ZERO),
			TOP(Size.Rate.of(0, 0.5)),
			TOP_RIGHT(Size.Rate.of(0, 1)),
			LEFT(Size.Rate.of(0.5, 0)),
			CENTER(Size.Rate.HALF),
			RIGHT(Size.Rate.of(0.5, 1)),
			BOTTOM_LEFT(Size.Rate.of(1, 0)),
			BOTTOM(Size.Rate.of(1, 0.5)),
			BOTTOM_RIGHT(Size.Rate.SAME);

			private final Size.Rate rate;

			private Base(Size.Rate rate){
				this.rate = rate;
			}

			private Position adjust(Position current, Size difference) {
				return current.minus(difference.multiply(rate).toPosition());
			}
		}

		private final Size.Rate rate;
		private final Base base;

		private Scaling(Size.Rate rate, Base base) {
			this.rate = rate;
			this.base = base;
		}

		static public Scaling of(Size.Rate rate, Base base) {
			return new Scaling(rate, base);
		}

		static public Scaling of(Size.Rate rate) {
			return new Scaling(rate, Base.TOP_LEFT);
		}

		public Rectangle apply(Position position, Size size) {
			Size multipliedSize = size.multiply(rate);
			Size differenceSize = multipliedSize.minus(size);

			return Rectangle.of(base.adjust(position, differenceSize), multipliedSize);
		}

	}

	public Rectangle resize(Size.Rate rate) {
		return resize(Scaling.of(rate));
	}

	public Rectangle resize(Scaling scaling) {
		return scaling.apply(position, size);
	}

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

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);
	}
	
	public Position minus(Position other) {
		return plus(other.negate());
	}

	public Position negate() {
		return of(-x, -y);
	}

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

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);
	}

	public Size minus(Size other) {
		return of(height - other.height, width - other.width);
	}

	public Position toPosition(Position offset) {
		return Position.of(height, width).plus(offset);
	}

	public Position toPosition() {
		return toPosition(Position.of(0, 0));
	}

	static public class Rate {
		private final double x;
		private final double y;

		static public final Rate ZERO = of(0);
		static public final Rate HALF = of(0.5);
		static public final Rate SAME = of(1);
		static public final Rate DOUBLE = of(2);

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

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

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

		private Size apply(double height, double width) {
			return Size.of(height * x, width * y);
		}
	}

	public Size multiply(Rate rate) {
		return rate.apply(height, width);
	}

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

コンソール出力。

Ver.009 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)
Rectangle (x, y) = (100.0, 100.0), (height, width) = (30.0, 45.0)

Rectangle (x, y) = (100.0, 92.5), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (100.0, 85.0), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (95.0, 100.0), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (95.0, 92.5), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (95.0, 85.0), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (90.0, 100.0), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (90.0, 92.5), (height, width) = (30.0, 45.0)
Rectangle (x, y) = (90.0, 85.0), (height, width) = (30.0, 45.0)

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

ビジネス的にどこまで必要か、なんてことは設計を始めた段階では神のみぞ知ることです。我々が出来ることは、その時点で最善の設計を行い、理解しやすい実装を行うことで、いざ修正が必要となった時のリスクを出来るだけ下げること、だと思います。
その為には、常日頃から、簡単な問題であっても全力でモデリングすることでスキルを磨く必要があるかと思います。塵も積もれば山となる、ってことです。

ちなみにここで行っているのはどうモデリングのサンプルとして図形をいじっているだけなので、実際にバリバリ図形を動かしたい方は既存のAPI使ったほうが良いです。興味があればアフィン変換とかが参考になります。

さて、次はどうしようかな。