四角を動かすだけ(5)。
前回↑の続き。
さて、次の機能追加は大きさを変更する際の起点を指定できるようにしてみたいと思います。前回、大きさを変更できるようにしましたが、Potisionが起点となっています。左上が(0, 0)のxy座標系とすると、四角形の左上の角の位置は変わらずに右下の角が動く感じです。
大きさの変更は四角の中心を起点に4つの角がそれぞれ移動する、としても妥当な動作かと思います。
折角なので、左上、上、右上、右、・・・と9つの点を起点として選択できるようにしてみます。
例えば中心が起点とすると、四角の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使ったほうが良いです。興味があればアフィン変換とかが参考になります。
さて、次はどうしようかな。
四角を動かすだけ(4)。
前回↑の続き。
ひとまず、四角を動かすことは出来ました。
次に何か適当な機能を追加してみようと思います。
何でもいいんですが、PositionとSizeを定義しておいてSizeは何もしていないので、取りあえず大きさを変えるような機能を追加します。
さっくり書くとやりたいのは、
// class Service Rectangle resizedRactangle = rectangle.resize(1.5, 1.5);
こういうことですかね。
その為にRectangleにresizeメソッドを追加します。
ここでもキッチリget()せずに求めるな、命じよを守って
// class Rectangle public Rectangle resize(double rateX, double rateY) { return of(position, size.multiply(rateX, rateY)); }
Sizeに掛け算するように命じます。
なので、Sizeにも掛け算のメソッドを追加することになります。
// class Size public Size multiply(double x, double y) { return of(height * x, width * y); }
と、ただそれだけなのですが、またしても x,y が常に同時に扱われているし、doubleだし、サイズを変更する責務を切り出して新しいクラスを導入してみます。適切にクラス化することで演算の誤りを防ぐことが出来ます。既に位置や大きさはクラス化されていますが、倍率と共にdoubleのままだとすると、足し算や引き算など望ましくない演算が可能となってしまいます。クラス化することで意図された演算のみをメソッドとして実装することで、予め不適切な演算を防ぎます。
今回はこの掛け算はSizeにのみ適用される演算であるとして、Sizeの内部クラスにします。
もちろん、Positionにも適用される共通な演算とみなす場合は内部クラスとするのは不適切です。
// class Service Size.Rate rate = Size.Rate.of(1.5); Rectangle resizedRactangle = rectangle.resize(rate);
// class Rectangle public Rectangle resize(Size.Rate rate) { return of(position, size.multiply(rate)); }
// class Size static public class Rate { private final double x; private final double y; 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); }
新たなクラスRateを導入することで、掛け算の倍率を管理することはRateの責務となります。
それをget()するようなことはせずに、倍率を適用するように命じることがポイントです。
機能追加後。
package com.ukiyu.blog008; public class Service { public static void main(String[] args) { System.out.println("Ver.008 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); } }
package com.ukiyu.blog008; 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)); } public Rectangle resize(Size.Rate rate) { return of(position, size.multiply(rate)); } @Override public String toString() { return "Rectangle " + position + ", " + size; } }
package com.ukiyu.blog008; 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.blog008; 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); } static public class Rate { private final double x; private final double y; 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.008 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)
コードはこちら↓。
https://github.com/ukiyu/blog/tree/master/ForBlog/src/com/ukiyu/blog008
(100, 100)の位置にあった(20, 30)の大きさの四角形が1.5倍になりました。ちゃんとモデリングすることでソースコードもそれをそのまま表現したものになっているかと思います。getter/setter を多用するコードにはそのような表現力はありません。
次回もまた適当な機能追加を行いたいと思います。
続く。
四角を動かすだけ(3)。
前回↑の続き。
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
コメントに書かれてあったことはコードで表現できるようになったのでコメントは消しました。
修正はこれで完了です。
今回は駄目なモデルからリファクタリングを行う形をとりましたが、
リファクタリングという遠回りをしなくても、この程度のことであれば最初から適切にモデリングを行いたいところです。
次からは自由気ままに適当な機能を追加していこうかな、と思っています。
続く。
四角を動かすだけ(2)。
前回↑の続き。
package com.ukiyu.blog002; public class Service { public static void main(String[] args) { System.out.println("Ver.002 Start"); // 座標(100, 100)に存在する20x30の大きさの四角形 Rectangle rectangle = Rectangle.of(100, 100, 20, 30); System.out.println(rectangle); // x方向に10動かす。 rectangle.addX(10); // y方向に20動かす。 rectangle.addY(20); System.out.println(rectangle); } }
package com.ukiyu.blog002; public class Rectangle { private double x; private double y; private double height; private 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 void addX(double x) { this.x += x; } public void addY(double y) { this.y += y; } @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/blog002
気になる点。
- 引数が多い。
- インスタンスが可変。
- x,y同時に動かしたいな。
x, y 同時に動かしたいな。
まぁ、全く難しい事ではないので、さくっと。
public void addXY(double x, double y) { this.x += x; this.y += y; }
rectangle.addXY(10, 20);
複数存在することで1つの概念を成すものは、常に同時に扱われるようにします。
インスタンスが可変。
addXY()を行うことによってインスタンスの状態が変化してしまいます。
これを防ぐためには常に新しいインスタンスを返すようにします。
public Rectangle addXY(double x, double y) { return of(this.x + x, this.y + y, this.height, this.width); }
// x方向に10, y方向に20動かす。 rectangle = rectangle.addXY(10, 20);
addXY()を呼んでるrectangleと戻り値を受け取るrectangleは異なるインスタンスを参照しており、
それぞれのインスタンスの状態は不変であることが分かるでしょうか?
分かりにくいです。NGです。
だから、ローカル変数を使いまわすなと・・・。
正しく、分かりやすく名前を付け直しましょう。addXY()する前と、した後は違う状態なのです。
名前が異なるのが当然です。
// x方向に10, y方向に20動かす。 Rectangle addedRectangle = rectangle.addXY(10, 20);
これでrectangleは最初に生成されてから、状態は常に不変であり、それが明白になりました。
もちろん、addedRectangleも常に不変です。
修正後。
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 + ")"; } }
コンソール出力。
Ver.004 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/blog004
さて、モデリングっぽい修正が必要な、
- 引数が多い。
点の修正は、また次回。
四角を動かすだけ(1)。
簡単なことだからこそ、きっちりやります。
本質が分かりやすいです。
そして、簡単なところ(練習)できっちり出来なければ、当然、難しいところ(本番)で出来るはずがない、と思っています。
四角を動かしてみます。
package com.ukiyu.blog001; public class Service { public static void main(String[] args) { System.out.println("Ver.001 Start"); // 座標(100, 100)に存在する20x30の大きさの四角形 Rectangle rectangle = new Rectangle(100, 100, 20, 30); System.out.println(rectangle); // x方向に10動かす。 double x = rectangle.getX(); x = x + 10; rectangle.setX(x); // y方向に20動かす。 double y = rectangle.getY(); y = y + 20; rectangle.setY(y); System.out.println(rectangle); } }
package com.ukiyu.blog001; public class Rectangle { private double x; private double y; private double height; private double width; public Rectangle(double x, double y, double height, double width) { this.x = x; this.y = y; this.height = height; this.width = width; } public double getX() { return x; } public void setX(double x) { this.x = x; } public double getY() { return y; } public void setY(double y) { this.y = y; } @Override public String toString() { return "Rectangle (x, y) = ( " + x + ", " + y + ") , (height, width) = ( " + height + ", " + width + ")"; } }
コンソール出力。
Ver.001 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/blog001
さて、一体いくつのツッコミを入れることが出来るでしょうか?
とりあえず、main()関数を眺めると
- newしてる。
- 引数が多い。
- getしてる。x 2
- ローカル変数を使いまわしてる。
- setしてる。x 2
と、すべての行にツッコミが入ります(確認用の出力は除く)。
newしてる。
まぁ、いきなり、コンストラクタってnewするでしょ?って話になりかねないのですが
書籍「Effective Java」によるとstatic ファクトリメソッドを使うことが推奨されています。
理由はいくつかあるのですが、コンストラクタが沢山ある場合、それぞれのコンストラクタは
引数が異なり、インスタンスの生成の仕方が異なります。
でも全てのコンストラクタの名前は同じなので
クラスの使用者が、引数によってどのようなインスタンスが生成されるかを判断せざるを得ません。
メソッドにすることによって適切な名称を与えることが出来、ユーザビリティ及び可読性が向上します。
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); }
Rectangle rectangle = Rectangle.of(100, 100, 20, 30);
引数が多い。
引数は出来るだけ少なくしましょう。4つも並んでいると間違えちゃいます。
100, 100, 20, 30 って何の数字の羅列なんでしょうね?ぱっと見、理解不能です。
ここでは修正は後回しにします。量が多くなりそうなので。。。
getしてる。
安易にgetしないで下さい。getしようとしているものは、そのインスタンスの責任の下で管理されています。
カプセル化、情報隠蔽はオブジェクト指向の本質の1つです。getメソッドはそれを破壊します。
getせずにどうやって目的を達成するか、を考えるのも設計するということの一部だと思います。
getしてしまうと、お外に情報が伝搬します。すなわち、変更修正があった際の影響範囲が広がってしまうのです。
getしないことによって内側に影響を留めることが出来るようになります。
ここでは求めるな、命じよ。(Tell, Don't Ask.)という原則に従います。
(オブジェクト指向エクササイズ、9つのルールにも"getしない"ルールがあります。)
public void addX(double x) { this.x += x; } public void addY(double y) { this.y += y; }
rectangle.addX(10); rectangle.addY(20);
rectangleが持っている値(責任)を求めるのではなく、それに対する操作を命じるのです。
ローカル変数を使いまわしてる。
上述のaddメソッドを追加したのでいなくなっちゃうのですが、同じ名前の変数を使いまわすと混乱を招くことがあるので、
個々にきっちり名前を付けてあげましょう、というお話です。
public int update(int current) { current = current + 1; return current; }
1行目のcurrentと2行目のcurrentと3行目のcurrent、一体どれが本当にcurrentなんでしょうか?
public int update(int current) { int updated = current + 1; return updated; }
こうやってちゃんと正しい名前を付けてあげることでコードの可読性が向上します。
付加的な効果として、前の値が上書きされなくなるのでデバッグしやすかったりもします。
setしてる。
これも、上述のaddメソッドを追加したのでいなくなっちゃうのですが、setすることによる問題と同様の問題がまだ残っています。
その問題というのはインスタンスの状態が可変(ミュータブル)であるということ。
インスタンスは出来るだけ不変(イミュータブル)であることが望まれます。
可変なインスタンスは何かしらの問題が発覚した際に、どの状態で問題が生じて、その状態はどこで作られたのか?を調べる必要があります。そして全ての状態に対するテストが必要となります。
不変であれば、問題が発覚した際の状態が問題であることは一目瞭然です。また、スレッドセーフなので容易に扱うことが出来ます。
また、オブジェクト指向エクササイズ、9つのルールにも"setしない"ルールがあります。
修正は容易ですが、お腹いっぱいになってきたので後回しにします。
修正後。
package com.ukiyu.blog002; public class Service { public static void main(String[] args) { System.out.println("Ver.002 Start"); // 座標(100, 100)に存在する20x30の大きさの四角形 Rectangle rectangle = Rectangle.of(100, 100, 20, 30); System.out.println(rectangle); // x方向に10動かす。 rectangle.addX(10); // y方向に20動かす。 rectangle.addY(20); System.out.println(rectangle); } }
package com.ukiyu.blog002; public class Rectangle { private double x; private double y; private double height; private 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 void addX(double x) { this.x += x; } public void addY(double y) { this.y += y; } @Override public String toString() { return "Rectangle (x, y) = ( " + x + ", " + y + ") , (height, width) = ( " + height + ", " + width + ")"; } }
コンソール出力。
Ver.002 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/blog002
この時点で気になるのは
- 引数が多い。
- インスタンスが可変。
- x,y同時に動かしたいな。
ってところでしょうか。
続く。