かなりの数の回答とコメントで述べられているように、DTO は状況によって、特に境界を越えてデータを転送する場合(たとえば、JSONにシリアル化してWebサービス経由で送信する場合)に適切で便利です。この答えの残りの部分では、多かれ少なかれそれを無視して、ドメインクラス、およびゲッターとセッターを最小化(排除しないにしても)し、それでも大規模プロジェクトで役立つように設計する方法について説明します。また、ゲッターまたはセッターを削除する理由、または削除するタイミングについても説明しません。これらは独自の質問であるためです。
例として、プロジェクトがチェスや戦艦のようなボードゲームであると想像してください。これをプレゼンテーション層(コンソールアプリ、Webサービス、GUIなど)で表すさまざまな方法がありますが、コアドメインもあります。あなたが持つかもしれない1つのクラスはCoordinate
、ボード上の位置を表します。「悪」の書き方は次のとおりです。
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(簡潔にするため、また、私はそれをよく知っているので、JavaではなくC#でコード例を作成します。それが問題ではないことを願っています。概念は同じで、翻訳は単純でなければなりません。)
セッターの削除:不変性
公開ゲッターとセッターはどちらも潜在的に問題がありますが、セッターはこの2つの「悪」です。通常、それらは除去するのが簡単です。このプロセスは、コンストラクター内から値を設定する単純なものです。以前にオブジェクトを変更したメソッドは、代わりに新しい結果を返す必要があります。そう:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
これは、XおよびYを変更するクラスの他のメソッドから保護されないことに注意してください。より厳密に不変にするには、readonly
(final
Javaで)を使用できます。ただし、プロパティを真に不変にするか、セッターを介して直接パブリックミューテーションを防止するだけでも、パブリックセッターを削除するトリックは実行されます。大多数の状況では、これはうまく機能します。
ゲッターの削除、パート1:動作の設計
上記はすべてセッターに適していますが、ゲッターに関しては、開始する前に実際に足を撃ちました。私たちのプロセスは、座標が何であるか(それが表すデータ)を考え、その周りにクラスを作成することでした。代わりに、座標から必要な動作から始める必要がありました。ちなみに、このプロセスはTDDによって支援されます。TDDでは、必要な場合にのみこのようなクラスを抽出するため、目的の動作から開始してそこから作業を行います。
だから、最初Coordinate
に衝突を検出する必要があると思った場合、2つの部品がボード上の同じスペースを占有しているかどうかを確認したいとしましょう。これが「邪悪な」方法です(簡潔にするためにコンストラクタは省略されています)。
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
そして、これが良い方法です:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
(IEquatable
実装は簡略化のために省略されています)。データをモデリングするのではなく、動作を設計することで、ゲッターを削除することができました。
これは例にも関連していることに注意してください。ORMを使用している場合や、Webサイトなどに顧客情報を表示している場合は、何らかのCustomer
DTOが意味をなすでしょう。しかし、システムに顧客が含まれており、それらがデータモデルで表されているからといってCustomer
、ドメインにクラスがある必要があることを自動的に意味するわけではありません。たぶん、振る舞いのために設計すると、1つが出現しますが、ゲッターを避けたい場合は、先制的に作成しないでください。
ゲッターの削除、パート2:外部の動作
したがって、上記のは良いスタートですが、遅かれ早かれ、あなたはおそらくあなたには、いくつかの方法で、クラスの状態に依存クラス、関連付けられている行動を持っているような状況に遭遇しますが、これは属していない上クラス。このような動作は、通常、アプリケーションのサービス層に存在します。
このCoordinate
例を使用すると、最終的にはユーザーにゲームを表現したいと思うでしょう。これは、画面に描画することを意味する場合があります。たとえば、Vector2
画面上のポイントを表すために使用するUIプロジェクトがあるとします。ただし、Coordinate
クラスが座標から画面上のポイントへの変換を担当することは不適切です。これにより、あらゆる種類のプレゼンテーションの問題がコアドメインに取り込まれます。残念ながら、このような状況はオブジェクト指向設計に固有のものです。
非常に一般的に選択される最初のオプションは、いまいましいゲッターを公開して、それで地獄に言うだけです。これには単純さという利点があります。しかし、ゲッターを避けることについて話しているので、議論のためにこれを拒否し、他にどんなオプションがあるか見てみましょう。
2番目のオプションは.ToDTO()
、クラスに何らかのメソッドを追加することです。とにかく、これまたは類似のものが必要になる可能性があります。たとえば、ゲームを保存する場合、ほとんどすべての状態をキャプチャする必要があります。しかし、サービスに対してこれを行うことと、ゲッターに直接アクセスすることの違いは、多かれ少なかれ見た目です。それにはまだ「悪」があります。
3つ目のオプション -Zoran Horvatが複数のPluralsightビデオで提唱しているのを見てきた- は、訪問者パターンの修正バージョンを使用することです。これはパターンの非常に珍しい使用とバリエーションであり、実際のゲインなしに複雑さを追加するのか、それとも状況の良い妥協であるのかによって、人々の走行距離は大きく変わると思います。基本的には、標準のビジターパターンを使用するという考え方ですが、Visit
メソッドは、訪問するクラスではなく、必要な状態をパラメーターとして取得します。例はここにあります。
私たちの問題では、このパターンを使用した解決策は次のとおりです。
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
あなたはおそらく言うことができる、通り_x
と_y
されていない、本当にこれ以上をカプセル化。IPositionTransformer<Tuple<int,int>>
それらを直接返すだけのを作成することで抽出できます。味によっては、これが運動全体を無意味にすると感じるかもしれません。
ただし、パブリックゲッターでは、データを直接引き出してTell、Do n't Askに違反して使用するだけで、間違った方法で物事を行うのは非常に簡単です。このパターンを使用すると、実際には正しい方法で実行する方が簡単です。動作を作成する場合は、それに関連付けられた型を作成することから自動的に開始されます。TDAの違反は非常に明らかに臭いであり、おそらくよりシンプルでより良いソリューションを回避する必要があります。実際には、これらのポイントは、ゲッターが奨励する「邪悪な」方法よりも、正しい方法でオブジェクト指向を行う方がはるかに簡単です。
最後に、最初は明らかではない場合でも、実際には、状態を公開する必要を回避するために、振る舞いとして必要なものを十分に公開する方法があるかもしれません。たとえば、Coordinate
パブリックメンバーのみが存在する以前のバージョンを使用するとEquals()
(実際には完全なIEquatable
実装が必要になります)、プレゼンテーションレイヤーに次のクラスを記述できます。
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
おそらく驚くべきことに、目標を達成するために座標から実際に必要なすべての動作は、等価性チェックでした!もちろん、このソリューションはこの問題に合わせて調整されており、許容可能なメモリ使用量/パフォーマンスについて想定しています。これは、一般的なソリューションの青写真ではなく、この特定の問題領域に適合する単なる例です。
繰り返しになりますが、実際にはこれが不必要な複雑さであるかどうかについて意見は異なります。場合によっては、このような解決策が存在しないか、非常に奇妙で複雑な場合があり、その場合は上記の3つに戻すことができます。