【问题标题】:Law of Demeter confusion with the simple classes得墨忒耳定律与简单类的混淆
【发布时间】:2016-03-21 15:59:38
【问题描述】:

我正在从事计算几何项目。我有代表几何对象的类:Point、LineSegment 和对这些对象执行计算的类:Geometry。我对得墨忒耳法则和“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 是否违反了 Demeter 法则?我想是的,因为它访问 InitialPoint 和 TerminalPoint 属性并将它们作为参数传递给MethodWithThreePoints,它访问这些点的 X 和 Y 属性。换句话说,MethodWithThreePoints 使用传递给类方法的对象属性的属性。

如果它违反了得墨忒耳法则,那么我看不到这个问题的最佳和合理的解决方案。我知道,我可以向 LineSegment 类添加额外的属性来满足 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到地狱,当然你必须访问对象的内部对象,一个段是两个点:一个点是N个坐标(对于N维),它更容易使用 MethodWithPointAndLineSegment(Point p1, LineSegment 段)比 MethodWithThreePoints(Point p1, Point p2, Point p3)
  • 令人困惑的是“法律”部分。与所有其他指南一样,它是一个指南,而且您似乎对 OOD 足够精通,可以理解在哪些情况下它更多的是一种责任而不是一种好处。
  • LoD 是关于隐藏实现细节,而不是数据结构。所以你不想要thing.subServiceX.subServiceY.doSomething()。但是 user.contactInfo.address.street 通常不被认为是违规行为;必须完全扁平化所有数据结构是愚蠢的。

标签: c# law-of-demeter


【解决方案1】:

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

我当然不会告诉你把它送到地狱去。它揭示了您设计中的问题。

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

我没有足够的信息来帮助你,但我可以告诉你你的 Point 和 LineSegment 类是简单的 POCO(没有行为,只有公共获取/设置)。 它只是没有行为的数据,这并不是真正的 OOP 友好。 它解释了为什么您很想在服务中操纵这些数据(即使您将服务称为“几何”)。

我猜你的代码中 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
    }
}

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

为了清楚起见,我并不是说你绝对必须在这里这样做,我只是解释为什么在你的情况下你很难尊重 Demeter。

【讨论】:

  • 在我的例子中,Point 和 Segment 只是数据结构(代表一对数字和点),而不是具有行为的对象。我认为,使用特定算法对点和段集执行计算不是单个点或段对象行为的一部分。这就是为什么我创建了单独的类(Geometry)来对几何对象执行计算,这些对象需要访问点、段和其他类似对象的内部对象。
  • 他们为什么需要访问它?他们不能要求这个结构做他们应该做的事情吗?
  • 例如X 和 Y 是计算距离或向量积所必需的,向量表示为 LineSegment(带有 InitialPoint 和 TerminalPoint)。距离有必要从给定的点数组中找到距离最小的点对。现在我想,你可能是对的,单个对象之间的一些计算可以通过 Point、LineSegment 等来完成。但是对 Points、LineSegments 等集合的计算可以通过像 Geometry 这样的外部类来完成,它可以调用点、线段等我会考虑的。
  • 这就是想法,但显然我需要完整的上下文才能更加相关。如果你只有一件事要保留,那就是:你不能总是遵循良好的做法,但你必须知道为什么要权衡取舍。
  • 这个答案帮助我意识到,我可以通过向几何对象添加功能来更好地设计我的类,而不是将所有内容委托给 Geometry 类(所以 +1)。但是,仍然存在直接访问属于某个对象的点的 X 和 Y 的情况,这样更简单、更智能。考虑LineSegment 类中的方法:double VectorProduct(LineSegment other)。这里我需要other 的端点的 X 和 Y。我认为,为每个点定义属于某个对象的附加属性和方法是个坏主意,所以我将直接访问它们。
【解决方案2】:

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

F# 从根本上建立在不可变数据的理念之上。因此,不是修改现有对象,任何“修改”实际上都会返回一个新对象。是的,性能受到了影响,但通常并不多; 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 }

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

F# 允许您在需要时作弊并将其设置为可变,并创建常规类/接口,但通常您不应该特别是在进行算法工作时。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多