得墨忒耳法则与简单的混淆 类

Law of Demeter confusion with the simple classes

我正在从事计算几何项目。我有 classes 表示几何对象:Point、LineSegment 和 class,它对这些对象执行计算:Geometry。我对 Demeter 法则和“MethodWithPointAndLineSegment”感到困惑。

class Point
{
    public int X { get; set; }
    public int Y { get; set; }  
    ...
}

class LineSegment
{
    public Point InitialPoint { get; set; }
    public Point TerminalPoint { get; set; }
    ...
}

class Geometry
{
    private ... MethodWithThreePoints(Point p1, Point p2, Point p3)
    {
        // accessing X and Y properties of passed points
        ...
    }

    public ... MethodWithPointAndLineSegment(Point p1, LineSegment segment)
    {
        MethodWithThreePoints(p1, segment.InitialPoint, segment.TerminalPoint);
        ...
    }
}

我的问题是:MethodWithPointAndLineSegment是否违反得墨忒尔法则?我想 yes,因为它访问 InitialPoint 和 TerminalPoint 属性并将它们作为参数传递给 MethodWithThreePoints,后者访问这些点的 X 和 Y 属性。换句话说,MethodWithThreePoints 使用传递给 class 方法的对象的属性。

如果它违反得墨忒耳法则,那么我看不出这个问题的最佳合理解决方案。我知道,我可以向 LineSegment class 添加其他属性以满足 LoD:

class LineSegment
{
    ...
    public int InitialPointX 
    { 
        get { return InitialPoint.X; }
        set { InitialPoint.X = value; }
    }
    //etc...
}

但是当我想在 MethodWithPointAndLineSegment 中调用 MethodWithThreePoints 时,它迫使我创建新点:new Point(segment.InitialPointX, segment.InitialPointY)...并将这些新点传递给 MethodWithThreePoints .它引入了一些额外的和不需要的性能成本,因为我必须创建新对象并将多级访问器返回的构造函数值传递给它们。

我不确定,这个问题和许多类似问题的最佳解决方案是什么:满足 LoD 或便利性,在这种情况下是性能(这些方法将在短时间内被多次调用以执行算法计算)。

欢迎提出任何建议和解释。

我同意说,与任何良好做法一样,LoD 是您必须适应您的环境的指南。

不过我当然不会叫你下地狱。它揭示了您设计中的问题。

如果您熟悉 OOP,您就会知道您应该设计高度连贯的 class,将数据和行为结合起来,并告诉他们该做什么(参见 Martin Fowler:http://martinfowler.com/bliki/TellDontAsk.html

我这里没有足够的信息来帮助你,但我可以告诉你你的 Point 和 LineSegment class 是简单的 POCO(没有行为,只有 public get/set)。 只有数据没有行为,这不是真正的 OOP 友好。 它解释了为什么您很想在服务中操作此数据(即使您调用服务 "Geometry")。

我猜你的代码中 Point 和 LineSegment 代表数据,Geometry 代表你想要的行为。

更面向对象的设计,假设添加翻译行为可能是这样的:

class Point : ITranslate
{
    public Point(int x, int y)
    {
        Y = y;
        X = x;
    }

    public int X { get; private set; }
    public int Y { get; private set; }

    public Point Translate(Translation translation)
    {
        //return a new translated point
    }
}

了解我如何为自己保留数据并公开所需的行为。这里我也设计了一个不可变的点,但是我们也可以这样做:

class Point : ITranslate
{
    public Point(int x, int y)
    {
        Y = y;
        X = x;
    }

    public int X { get; private set; }
    public int Y { get; private set; }

    public void Translate(Translation translation)
    {
        //apply the translation to myself internally on my X and Y
    }
}

当然,我需要更多的上下文才能更有帮助,但我希望它已经回答了你的问题。

明确一点,我并不是说你绝对必须在这里这样做,我只是解释为什么你觉得在你的情况下很难尊重得墨忒耳。

您可以考虑为此使用 F#。通过使用单独的 Geometry class 你已经完成了一半。

F# 从根本上建立在不可变数据的思想之上。因此,不是修改现有对象,而是 "modification" 实际上 returns 一个新对象。是的,性能受到了影响,但通常影响不大; HFT 算法需要快速但经常使用 F#/Haskell/OCaml,因为减速可以忽略不计,而且生成的代码更容易推理。

您的 F# 代码类似于

type Point = {X: int; Y: int}
type Line = {Start: Point; End: Point}
module Geometry =
  // Some specific examples
  let MiddleOfThreePoints p1 p2 p3 = {X=(p1.X + p2.X + p3.X)/3; Y=(p1.Y + p2.Y + p3.Y)/3}
  let MiddleOfLineAndPoint l p = MiddleOfThreePoints l.Start l.End p
  // You can even use currying to shorten that:
  let MiddleOfLineAndPoint l = MiddleOfThreePoints l.Start l.End

  // Note this returns a *new* point, doesn't modify the existing
  let TranslatePoint x y p = {X = p.X + x; Y = p.Y + y}
  // This returns a *new* list of points; doesn't modify anything
  let TranslatePoints x y points = points |> Seq.map(fun p -> TranslatePoint x y p)
  // A shorter version by using partial application on TranslatePoint
  let TranslatePoints x y points = points |> Seq.map(TranslatePoint x y)
  // An even shorter version by using currying
  let TranslatePoints x y = Seq.map(TranslatePoint x y)

  // "Modify" your object using "with" syntax (creates new object, but convenient syntax)
  let SetY p y = { p with Y = y }

不可变性在谈论多线程访问(不用担心并发模组,因为没有模组)、能够撤消更改(只保留以前版本的列表)以及理解正在发生的事情( 什么都没有)。此外,F# 语法简洁明了。

F# 允许您在需要时作弊并将事物设置为可变,并创建常规 classes/interfaces,但通常您不应该这样做,尤其是在进行算法工作时。