如很多答案和评论中所述,DTO 在某些情况下是适当且有用的,尤其是在跨边界传输数据时(例如,序列化为JSON以通过Web服务发送)。对于此答案的其余部分,我或多或少会忽略它,而谈论领域类,以及如何设计它们以最小化(如果不消除)getter和setter,并在大型项目中仍然有用。我也不会谈论为什么删除getter或setter或何时删除,因为这是他们自己的问题。
例如,假设您的项目是象棋或战舰这样的棋盘游戏。您可能有多种在表示层(控制台应用程序,Web服务,GUI等)中表示此内容的方法,但您也有一个核心域。您可能有一个班级Coordinate
,代表董事会中的职位。编写它的“邪恶”方法是:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(为了简洁起见,我将用C#而不是Java编写代码示例,因为我对此更加熟悉。希望这不是问题。概念相同,翻译应该简单。)
消除二传手:不变性
尽管公共获取者和设置者都可能存在问题,但设置者却是两者中更为“邪恶”的。它们通常也更容易消除。这个过程很简单,只需在构造函数中设置值即可。任何先前使对象发生突变的方法都应返回新结果。所以:
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
示例为例,最终您将需要向用户展示您的游戏,这可能意味着要在屏幕上绘画。例如,您可能有一个UI项目,该项目Vector2
用于表示屏幕上的一个点。但是,让Coordinate
班级负责将坐标从屏幕上的点转换为屏幕上的点是不合适的,因为这会将各种表示形式的关注点带入您的核心领域。不幸的是,这种情况是OO设计中固有的。
第一种选择是非常普遍的选择,它只是暴露该死的吸气剂,并对此大声疾呼。这具有简单的优点。但是,由于我们正在谈论避免使用吸气剂,因此,为了争辩而说,我们拒绝这一方法,然后看看还有哪些其他选择。
第二种选择是.ToDTO()
在类上添加某种方法。无论如何,都可能需要(或类似的)方法,例如,当您要保存游戏时,需要捕获几乎所有状态。但是,为您的服务执行此操作与直接访问getter之间的区别或多或少是出于美观目的。它仍然具有同样的“邪恶”。
第三种选择 -使用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的行为将非常明显,并且可能需要采用更简单,更好的解决方案。在实践中,这些要点使正确的方法(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];
}
}
事实证明,也许令人惊讶的是,所有我们的行为确实从一个坐标需要实现我们的目标是平等的检查!当然,此解决方案适合于此问题,并假设可接受的内存使用/性能。这只是适合此特定问题领域的一个示例,而不是一般解决方案的蓝图。
再次,关于在实践中这是否不必要的复杂性,意见会有所不同。在某些情况下,可能不存在这样的解决方案,或者它可能过于怪异或复杂,在这种情况下,您可以恢复到上述三种。