使用单个值对象的不同表示

Using Different Representations of a Single Value Object

我在工业自动化领域工作。该软件将使用每台设备制造商设计的串行连接和协议来控制多台工业设备。我已经为常用参数(例如距离、电压等)创建了值对象,但是,每台设备都需要不同的值表示,UI 还有一个。

举个例子,假设我需要将一个物体的位置调整1厘米。设备 #1 期望距离以米为单位,设备 #2 期望距离以微米为单位,UI 期望距离以厘米为单位。我计划使用 MKS 系统,以便我的距离值对象以米为单位存储数量。

从 Vaughn Vernon 的实施领域驱动设计一书中,我的值对象似乎应该满足他所描述的值对象的特征,即:

  1. 它测量、量化或描述领域中的事物。
  2. 它可以保持不变。
  3. 它通过将相关属性组合为一个整体单元来建模概念整体。
  4. 当测量或描述发生变化时,它是完全可替换的。
  5. 可以使用Value equality与其他人进行比较
  6. 它为其合作者提供无副作用的行为

想法 1: 为每个值对象添加十二个常用度量前缀 class。

public class Distance
{
    private readonly double quantity; // in meters

    public Distance(double distance)
    {
        quantity = distance;
    }

    public double AsCentimeters()
    {
        return quantity * 100;
    }
}

这种方法似乎不正确,因为基于前缀的计算不会改变,并且必须在多个值对象中重复。

想法 2: 引入一个带有度量前缀的枚举和一个带有计算的基 class。

public enum SIPrefix
{
    None, Centi 
};

public class SIUnitBase
{
    protected readonly double quantity;

    public double Value(SIPrefix prefix)
    {
        switch (prefix)
        {
            case SIPrefix.Centi:
                return quantity * 100;
                break;
            default:
                return quantity;
                break;
        }
    }
}

public class Distance : SIUnitBase
{
    public Distance(double distance)
    {
        quantity = distance;
    }
}

// ... in code ...
Distance d = new Distance(1.0, SIPrefix.None);
Equipment.Move(d.Value(SIPrefix.Centi));

这种方法看起来既冗长又容易出错。

想法 3: 创建一组扩展方法以添加所需的功能。

public static class DistanceExtensions
{
    public static double AsCentimeters(this Distance distance)
    {
        return distance * 100;
    }
}

这种方法看起来几乎和 想法 1 一样冗长,但我只能实现特定应用程序所需的转换。

我应该如何为这个架构建模,以便我可以提供每个域对象和 UI 它期望的表示中的值对象?

搜索以前的值对象问题只显示 one thread 与我的问题相似。

我认为您缺少单位和转换率的概念。我不太了解c#语法,所以我只是在JavaScript:

中写了示例

//This would be an enum holding conversion ratios to meters
var units = {
  cm: 0.01,
  dm: 0.1,
  m: 1,
  km: 1000,
  assertValidUnit: function(unit) {
    if (!this.hasOwnProperty(unit)) throw new Error('Invalid unit: ' + unit);
  }
};


function Distance(value, unit) {
  units.assertValidUnit(unit);
  this.value = value;
  this.unit = unit;
}

Distance.prototype = {
  constructor: Distance,
  
  in: function(unit) {
    units.assertValidUnit(unit);
    return new Distance(this.value * (units[this.unit] / units[unit]), unit);
  },

  plus: function (distance) {
      var otherDistanceValueInCurrentUnit = distance.in(this.unit).value;

      return new Distance(this.value + otherDistanceValueInCurrentUnit, this.unit);
  },

  minus: function (distance) {
      var otherDistanceValueInCurrentUnit = distance.in(this.unit).value;

      return new Distance(this.value - otherDistanceValueInCurrentUnit, this.unit);
  },

  toString: function() {
    return this.value + this.unit;
  }
};

var oneKm = new Distance(1, 'km'),
  oneM = new Distance(1, 'm'),
  twoCm = new Distance(2, 'cm');

log(oneM + ' + ' + oneKm + ' is ' + oneM.plus(oneKm));
log(oneM + ' - ' + twoCm + ' is ' + oneM.minus(twoCm).in('cm'));

convertAndLog(oneKm, 'm');
convertAndLog(oneKm, 'cm');
convertAndLog(oneM, 'dm');
convertAndLog(oneM, 'km');


function convertAndLog(distance, unit) {
  var convertedDistance = distance.in(unit),
      msg = distance + ' is ' + convertedDistance;
  
  log(msg);
}

function log(msg) {
  document.body.appendChild(document.createTextNode(msg));
  document.body.appendChild(document.createElement('br'));
}

您的 Equipment 可以直接处理 Distance 并在内部使用 distance.in 以确保它使用正确的单位。

function Equipment(distanceUnit) {
    this.distanceUnit = distanceUnit;
}

Equipment.prototype.move = function (distance) {
    alert('Moved equipment by ' + distance.in(this.distanceUnit));
};

var equipment = new Equipment('cm');

equipment.move(new Distance(5.4, 'dm')); //Moved equipment by 54cm

它可能根本不是惯用的 C#,但这是一个尝试:

//Values stored as unit/m * 1000 to avoid enum float limitations
public enum MeasureUnit {cm = 10, dm = 100, m = 1000, km = 1000000}

public static class MesureUnitExtension {
  public static float Ratio(this MeasureUnit e) {
    return (float)e / (float)1000.0;
  }
}

public class Measure {
    private float value;
    private MeasureUnit unit;

    public Measure(float value, MeasureUnit unit) {
        this.value = value;
        this.unit = unit;
    }

    public Measure In(MeasureUnit unit) {
        return new Measure(this.value * (this.unit.Ratio() / unit.Ratio()), this.unit);
    }

    public override string ToString() {
        return this.value.ToString() + this.unit.ToString();
    }
}

你的值对象的设计很好。封装例如一个距离并为主要单位(例如米)提供一个访问器是所有值对象所需要的。

软件本身应该能够仅使用这些值对象(无需访问原始值)来维护有意义的模型。因此您可能需要加法和减法运算符这些价值对象也是如此,可能还有其他人。 这里的目标是 VO 的用户永远不需要考虑使用的具体单位(除了明显的情况,例如构造)。

但是不同的 Equipment 需要不同的表示这一事实不是您应该使用额外的访问器(或扩展方法)来解决的问题,因为这会使您的 VO 变得混乱,结果您的模型变得不太简洁。

我建议您为 Equipment 定义一个使用您的 VO 的接口。然后,创建一个知道 Equipment 的特定单位的 adapter。该适配器负责在您的标准化 VO 和 Equipment.

的细节之间进行转换