꽤 많은 답변과 의견에서 언급했듯이 DTO 는 일부 상황에서, 특히 경계를 넘어 데이터를 전송하는 경우 (예 : 웹 서비스를 통해 JSON으로 직렬화)에 적합하고 유용합니다. 이 답변의 나머지 부분에서는 도메인 클래스를 무시하고 도메인 클래스와 게터와 세터를 최소화 (제거하지 않을 경우)하도록 설계하고 대규모 프로젝트에서 여전히 유용하게 사용할 수있는 방법에 대해 이야기하겠습니다. 또한 게터 나 세터를 왜 제거 해야하는지 , 언제 그렇게 해야하는지에 대해서는 이야기하지 않을 것 입니다.
예를 들어, 프로젝트가 체스 또는 전함과 같은 보드 게임이라고 가정하십시오. 프리젠 테이션 레이어 (콘솔 앱, 웹 서비스, GUI 등)에서이를 나타내는 다양한 방법이있을 수 있지만 핵심 도메인도 있습니다. 당신이 가질 수있는 한 클래스 Coordinate
는 보드의 위치를 나타냅니다. 그것을 쓰는 "악"방법은 다음과 같습니다.
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(저는 간결하게하기 위해 Java 대신 C #으로 코드 예제를 작성하려고합니다. 더 익숙해지기를 바랍니다. 문제가되지 않기를 바랍니다. 개념은 같고 번역은 간단해야합니다.)
세터 제거 : 불변성
퍼블릭 게터와 세터는 모두 잠재적으로 문제가 있지만 세터는 둘 중 훨씬 더 "악"입니다. 또한 일반적으로 제거하기가 더 쉽습니다. 프로세스는 생성자 내에서 값을 설정하는 간단한 것입니다. 이전에 객체를 변경 한 메소드는 대신 새로운 결과를 반환해야합니다. 그래서:
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
충돌 감지가 필요한 곳이라고 가정 해 봅시다 . 두 조각이 보드의 동일한 공간을 차지하는지 확인하고 싶었습니다. 다음은 "이블"방식입니다 (간결하게하기 위해 생성자가 생략 됨).
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을 사용 중이거나 웹 사이트 또는 기타 정보에 고객 정보를 표시 할 수 있습니다.이 경우 어떤 종류의 Customer
DTO가 적합 할 수 있습니다. 그러나 시스템에 고객이 포함되어 있고 데이터 모델에 고객이 있다고해서 Customer
도메인에 클래스 가 있다는 것을 자동으로 의미하지는 않습니다 . 어쩌면 행동을 디자인 할 때 나타날 수도 있지만, 게터를 피하려면 미리 선제 적으로 만들지 마십시오.
게터 제거, 2 부 : 외부 행동
그래서 위의 좋은 시작이지만, 조만간 당신은 아마 당신이 어떤 방법으로 클래스의 상태에 의존하는 클래스와 연관된 동작을 상황에 실행되지만 어떤 속하지 않는 에서 클래스. 이러한 종류의 동작은 일반적으로 응용 프로그램 의 서비스 계층 에 존재합니다.
우리의 Coordinate
예를 들어, 결국 당신은 게임을 사용자에게 표현하고 싶을 것입니다. 예를 들어, Vector2
화면에서 점을 나타내는 데 사용하는 UI 프로젝트가있을 수 있습니다 . 그러나 Coordinate
수업에서 좌표에서 화면상의 지점으로의 변환을 담당 하는 것은 부적절 할 것입니다. 이로 인해 모든 종류의 프리젠 테이션 문제가 핵심 영역에 포함됩니다. 불행히도 이러한 유형의 상황은 OO 디자인에 내재되어 있습니다.
가장 일반적으로 선택되는 첫 번째 옵션 은 지독한 게터를 노출시키고 지옥과 대화하는 것입니다. 이것은 단순성의 이점이 있습니다. 그러나 우리가 게터를 피하는 것에 대해 이야기하고 있기 때문에 논증을 위해 이것을 거부하고 다른 옵션이 있는지 봅시다.
두 번째 옵션 은 .ToDTO()
클래스에 어떤 종류의 메소드 를 추가하는 것입니다 . 예를 들어 게임을 저장하려면 거의 모든 상태를 캡처해야합니다. 그러나 서비스를 위해이 작업을 수행하고 게터에 직접 액세스하는 것의 차이점은 다소 미적입니다. 그것은 여전히 많은 "악"을 가지고 있습니다.
두 번째 Pluralsight 비디오에서 Zoran Horvat 가 옹호 한 세 번째 옵션 은 방문자 패턴의 수정 된 버전을 사용하는 것입니다. 이것은 패턴의 매우 특이한 사용 및 변형이며 사람들의 마일리지는 실제 이득이없는 복잡성을 추가하는지 또는 상황에 대한 훌륭한 타협인지에 따라 크게 달라질 것이라고 생각합니다. 아이디어는 기본적으로 표준 방문자 패턴을 사용하는 것이지만 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 위반은 매우 명백하게 냄새가 나며 아마도 더 간단하고 더 나은 해결책을 찾아야 할 것입니다. 실제로, 이러한 요점은 게터가 권장하는 "악한"방법보다 OO를 올바르게 수행하는 것이 훨씬 쉽습니다.
마지막으로 , 처음에는 명확하지 않더라도 실제로 는 상태를 노출 할 필요를 피하기 위해 행동으로 필요한 것을 충분히 노출시키는 방법이있을 수 있습니다 . 예를 들어 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];
}
}
놀랍게도, 목표를 달성하기 위해 좌표에서 실제로 필요한 모든 행동 은 평등 검사였습니다! 물론이 솔루션은이 문제에 맞게 조정되었으며 수용 가능한 메모리 사용량 / 성능에 대한 가정을합니다. 일반적인 솔루션의 청사진이 아니라이 특정 문제 영역에 맞는 예제 일뿐입니다.
그리고 다시 말하지만 실제로 이것이 불필요한 복잡성인지에 따라 의견이 달라질 것입니다. 어떤 경우에는 이와 같은 해결책이 존재하지 않거나 엄청나게 이상하거나 복잡 할 수 있습니다.이 경우 위의 세 가지로 되돌릴 수 있습니다.