Reputation: 610
I'm working on computational geometry project. I have classes representing geometrical objects: Point, LineSegment and class, which performs calculations on those objects: Geometry. I'm confused with the Law of Demeter and "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);
...
}
}
My question is: does MethodWithPointAndLineSegment
violates the Law od Demeter? I suppose yes, because it accesses InitialPoint and TerminalPoint properties and pass them as parameters to the MethodWithThreePoints
, which accesses X and Y properties of those points. In the other words, MethodWithThreePoints
uses properties of properties of objects passed to the class method.
If it violates the Law of Demeter, then I can't see best and reasonable solution for this problem. I know, I can add additional properties to the LineSegment class to satisfy LoD:
class LineSegment
{
...
public int InitialPointX
{
get { return InitialPoint.X; }
set { InitialPoint.X = value; }
}
//etc...
}
But when I want to invoke MethodWithThreePoints
within MethodWithPointAndLineSegment
it forces me to create new points: new Point(segment.InitialPointX, segment.InitialPointY)
... and pass those new points to the MethodWithThreePoints
. It introduces some additional and unwanted performance cost, because I have to create new objects and pass to them constructors values returned by the multi-level accessors.
I'm not sure, what would be the best solution for this problem and for many similar problems: satisfy LoD or convenience and in this case performance (those methods will be invoked many times in a short period for performing algorithms calculations).
Any suggestions and explanations are welcome.
Upvotes: 0
Views: 506
Reputation: 10781
You could look at using F# for this. By using a separate Geometry
class you're halfway there already.
F# is fundamentally built on the idea of immutable data. So instead of modifying your existing objects, any "modification" actually returns a new object. Yes there's a performance hit, but it's often not much; HFT algorithms need to be fast but frequently use F#/Haskell/OCaml because the slowdown is fairly negligible and the resulting code is much easier to reason about.
Your code in F# would look something like
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 }
Immutability has some great advantages when talking about multithreaded access (no worry about concurrent mods because there aren't mods), being able to undo changes (just keep a list of previous versions), and understanding what's going on (nothing is going on). Plus the F# syntax is nice and concise.
F# allows you to cheat and set things as mutable when you need to, and to create regular classes/interfaces, but in general you shouldn't especially when doing algorithmic work.
Upvotes: 0
Reputation: 3043
I would agree to say LoD is, as any good practices, a guideline you must adapt to your context.
I will certainly not tell you to send it to hell though. It reveals problems in your design.
If you are familiar with OOP, you know you should design highly coherent class which combine data and behavior, and tell them what to do (see Martin Fowler: http://martinfowler.com/bliki/TellDontAsk.html)
I have not enough information here to help you much, but I can tell you your Point and LineSegment class are simple POCO (no behaviour, only public get/set). It is only data without behavior, which is not really OOP friendly. It explains why you are tempted to manipulate this data in a service (even if you call your service "Geometry").
I Guess in your code Point and LineSegment represent the data, and Geometry represent the behavior you'd like to have.
A more OO design, let's say to add a translation behavior could be something like:
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
}
}
See how I keep my data for myself and just exposed the required behavior. Here I also design an immutable point, but we could also do something like this:
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
}
}
Of course I would need more context to be more helpful, but I hope it already answers your question.
Just to be clear, I don't say you absolutely have to do that here, I just explain why you find it hard to respect Demeter in your case.
Upvotes: 1