如果您希望以一种非常类似于人类语言语义的方式来表达您的逻辑,您需要在一个逻辑上能够执行它的实体上调用一个动作或函数。
当行为不能被放置在一个对象上时(从某种意义上说它有状态),你把它放在一个服务或实用程序类中,或者一些类似的构造中。 Authenticate 是一个典型的例子,它在用户或任何其他对象上调用没有多大意义。为此,我们创建了一个AuthenticationProvider(或服务,随你喜欢)。
在您的 Person 和 Car 场景中,它是一个对象调用另一个对象的行为。因此,person.Drive(car) 最有意义。
如果一个人拥有一辆汽车(并且汽车总是由一个人拥有),那么person.Drive() 可能是您唯一需要做的事情。 Drive() 方法将可以访问person 的属性,其中之一是它的car。
这里要注意的重要一点是松散耦合的概念。在更复杂的场景中,您不想在模型中进行各种交叉引用。但是通过使用接口和抽象,您经常会发现自己将方法放在从现实世界的角度来看并不真正属于的对象上。诀窍是要意识到并利用一种语言的特性来同时实现松散耦合和现实语义。
请记住,在实际应用程序中,您会将引导代码隐藏在其他地方,下面是 C# 中的示例:
我们首先为可传输的事物 (ITransporter) 和可传输的事物 (ITransportable) 定义接口:
public interface ITransportable
{
void Transport(Transportation offset);
}
public interface ITransporter
{
void StartTransportation(ITransportable transportable);
void StopTransportation(ITransportable transportable);
}
注意Transportation 帮助类,它包含在以一定速度传输一段时间后重新计算ITransportable 的当前位置所需的信息等等。一个简单的例子:
public class Transportation
{
public double Velocity { get; set; }
public TimeSpan Duration { get; set; }
}
然后我们继续为这些创建我们的实现。您可能已经猜到了,Person 将派生自 ITransportable 而Car 派生自 ITransporter:
public class Person : ITransportable
{
public Tuple<double, double> Location { get; set; }
private ITransporter _transporter;
void ITransportable.Transport(Transportation offset)
{
// Set new location based on the offset passed in by the car
}
public void Drive<TCar>(TCar car) where TCar : ITransporter
{
car.StartTransportation(this);
_transporter = car;
}
public void StopDriving()
{
if (_transporter != null)
{
_transporter.StopTransportation(this);
}
}
}
密切注意我在那里所做的事情。我在Person 类上提供了一个显式接口实现。这意味着 Transport 只能在此人实际被引用为 ITransportable 时被调用 - 如果您将其引用为 Person,则只有 Drive 和 StopDriving 方法可见。
现在是汽车:
public class Car : ITransporter
{
public double MaxVelocity { get; set; }
public double Acceleration { get; set; }
public string FuelType { get; set; }
private Dictionary<ITransportable, DateTime> _transportations = new Dictionary<ITransportable, DateTime>();
void ITransporter.StartTransportation(ITransportable transportable)
{
_transportations.Add(transportable, DateTime.UtcNow);
}
void ITransporter.StopTransportation(ITransportable transportable)
{
if (_transportations.ContainsKey(transportable))
{
DateTime startTime = _transportations[transportable];
TimeSpan duration = DateTime.UtcNow - startTime;
var offset = new Transportation
{
Duration = duration,
Velocity = Math.Max((Acceleration*duration.Seconds), MaxVelocity)/2
};
transportable.Transport(offset);
_transportations.Remove(transportable);
}
}
}
按照我们之前设置的指南,Car 上也不会有任何(可见的)方法。除非您将其明确引用为 ITransporter,这正是 Person 的 Drive 和 StopDriving 方法内部发生的情况。
所以这里的汽车只是汽车。它有一些属性,就像一辆真正的汽车,你可以根据这些属性确定一个人驾驶它一定时间后的位置偏移。汽车不能“驾驶”、“启动”或类似的东西。人对汽车这样做——汽车不会对自己这样做。
为了使其更真实,您必须添加各种额外的元数据,这些元数据会影响汽车在特定路线上特定时间段内的平均速度。事实是,无论如何,您可能最终都不会建模这样的东西。我坚持使用您的模型只是为了说明如果您使用的对象很难保留自然语言语义。
客户如何使用这些类的示例:
Person person = new Person();
Car car = new Car();
// car.Transport(); will not compile unless we explicitly
// cast it to an ITransporter first.
// The only thing we can do to set things in motion (no pun intended)
// is invoke person.Drive(car);
person.Drive(car);
// some time passes..
person.StopDriving();
// currentLocation should now be updated because the Car
// passed a Transportation object to the Person with information
// about how quickly it moved and for how long.
var currentLocation = person.Location;
正如我之前已经避开的那样,这绝不是这个特定场景的一个很好的实现。但是,它应该说明如何解决您的问题的概念:将“传输”逻辑保留在“传输器”内部,而无需通过公共方法公开该逻辑。这为您的客户端代码提供了自然语言语义,同时保持了适当的关注点分离。
有时您只需要利用您拥有的工具发挥创造力。