面向对象编程:如何正确设计、实现和命名涉及对象交互的方法?

Object-Oriented Programming: How to properly design, implement, and name a method which involve object interactions?

语言无关紧要,这是通用的面向对象问题(以 java/C# 等为例)。举一个简单的概念。 一个人有一辆车。人可以驾驶汽车。汽车通常不会开车或四处游荡,对吧? `` 但是,通常在代码中,我们会看到像 myCarObject.Drive().

这样的方法

现在当一个人被介绍时,这个人开车:

======================= First Way =================================
class Car{
    int odometer;void drive(){ odometer++; } 
}
class Person{
    void driveCar(Car c) { c.drive(); }
}
========================================================================

================================ Alternative Way =======================

public Car{ 
    int odometer;   // car doesn't do the driving, it's the person, so no drive()
}
public Person{
    void driveCar(Car c) { c.odometer++; }
}

==========================和其他方式....============ ================

============================================= ==============================

所以,我的问题很明确:在类似情况下 design/implement/name 方法的最佳方法是什么?

让像这样的简化示例变得有意义有点困难,但这是一个尝试:

A Car class 通常包含对象可以利用其拥有的信息自行完成的事情的方法,例如:

public class Car {

  private bool engineOn;

  public int Speed { get; private set; }

  public void Start() { engineOn = true; Speed = 0; }
  public void Accelerate() { Speed++; }
  public void Break() { if (Speed > 0) Speed--; }
  public void Stop() { Speed = 0; engineOn = false; };

}

A Person class 将通过控制汽车​​本身在其环境中不知道的事物来管理汽车。示例:

public class Person {

  public void Drive(Car car, int speedLimit) {
    car.Start();
    while (car.Speed < speedLimit) {
      car.Accelerate();
    }
    while (car.Speed > 0) {
      car.Break();
    }
    car.Stop();
  }

}

在每种情况下如何使用 OO 当然有许多不同的变体。

如果您希望以非常类似于人类语言语义的方式表达您的逻辑,您将希望在逻辑上能够执行它的实体上调用一个动作或函数。

当行为不能放在对象上时(在某种意义上它 有状态 ),你可以把它放在服务或实用程序 class 或一些类似的构造。 Authenticate 是一个 classic 示例,它对用户或任何其他对象调用没有多大意义。为此,我们创建了一个 AuthenticationProvider(或服务,以您喜欢的为准)。

在你的人和车场景中,它是一个对象调用另一个对象的行为。 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 助手 class,其中包含重新计算 ITransportable 当前位置所需的信息一定的速度等等。一个简单的例子:

public class Transportation
{
    public double Velocity { get; set; }
    public TimeSpan Duration { get; set; }
}

然后我们继续为这些创建我们的实现。正如您可能已经猜到的,Person 将派生自 ITransportableCar 派生自 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 class 上提供了一个显式接口实现。这意味着 Transport 只能在该人实际被引用为 ITransportable 时调用 - 如果您将其引用为 Person,则只有 DriveStopDriving 方法可见。

现在的车:

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 的 DriveStopDriving 方法内部发生的事情。

所以这里的 Car 就是 Car。它有一些属性,就像一辆真正的汽车,你可以根据这些属性确定一个人驾驶它一定时间后的位置偏移量。汽车不能 "Drive"、"Start" 或类似的东西。人对汽车这样做 - 汽车不会对自己这样做。

为了使其更逼真,您必须添加各种额外的元数据,这些元数据会影响汽车在特定时间段内在特定路线上的平均速度。事实是,您可能最终不会像这样建模。我坚持使用您的模型只是为了说明如果您使用的对象很难保留自然语言语义。

客户如何使用这些 classes 的示例:

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;

正如我之前已经回避的那样,这绝不是该特定场景的良好实现。但是,它应该说明如何解决您的问题的概念:将 "transportation" 的逻辑保留在 "transporter" 内部,而不需要通过 public 方法公开该逻辑。这为您的客户端代码提供了自然语言语义,同时保留了适当的关注点分离。

有时您只需要利用现有的工具发挥创意。

在第二种情况下,这就像你说驾驶汽车的任务在于增加里程表。这显然不是 driver 的事,并且违反了封装。里程表应该是一个实现细节。

第一种情况,汽车可能不会自己开车,但它会前进,所以你可以用另一个动词。但是 car.advance() 可能不是一个人开车的方式……即使是通过语音命令,命令的解码也可能会产生一系列更基本的命令。

我非常喜欢 Guffa 的回答,它试图说明开车可能意味着什么。但当然,你可能有另一个上下文......