I am fairly new to OO design and have problems with the design of some software and looking for a pattern or a combination of patterns that could help me solving my problem.
I have a type that has a collection of different geometric shapes (say lines, rectangles, and cubes). I actually even have composites of these shapes.
Each shape has one or more values. A value is a coordinate (between one-dimensional for lines and three-dimensional for cubes).
All types of shapes have two methods that are essential. A method for adding values and a method for getting all values of a shape. E.g. Cube.AddValue(new Value3D{X=4,Y=7,Z=11})
or Line.AddValue(new Value1D{X=42})
respectively Cube.GetValues()
would return a collection of Value3D
and Line.GetValues()
would return a collection of Value1D
.
Since these methods have fundamentally different signatures, I can’t just derive from a single interface to store all of these different objects in one collection. Splitting the collection into multiple separate collections, one for each type would be possible, but not desirable since they belong together (domain wise).
I’ve considered using three-dimensional values for all shapes and just ignoring Y/Z for lines/rectangles, but that isn’t really nice and would actually violate the O and L in SOLID (a 4th dimension is possibly needed in the future).
Another possibility would probably be to use a generic Interface like IShape<T>
where T
is Value*D. This interface would look like:
public interface IShape<T> {
public IEnumerable<T> GetValues();
public void AddValue(T value);
}
But this would require another, non-generic interface INonGenericShape {}
because I can’t use a generic interface without specifying the type. I would end up with something like this:
public class SomeShape{
private IList<INonGenericShape> _shapes = new List<INonGenericShape>();
}
But then I could as well use System.Object
, since IShape does not provide any valuable information. So here I am, not knowing how to solve this (storing the objects in a single collection for consumers of that class). Can anyone provide some idea or insight?
2
Contrary to many (perhaps most) object-oriented design tutorials out there, inheritance hierarchies should not necessarily match up with a domain’s real-world taxonomy. In other words, just because all your objects are shapes in the real world doesn’t necessarily mean they should all inherit from IShape
in your program. Even if they do all share an interface, just because they have similar methods doesn’t mean those methods need to be part of that interface.
The trick is to look at the calling code to see what operations you really need at that point. For example:
for shape in shapes:
shape.doSomething()
Are you really going to call AddValue
in a loop like that? I doubt it. More likely, it’s going to be something like Draw
or GetEditControl
, and AddValue
will be called before it’s even put into shapes
, or by a dimension-aware EditControl
. In other words, IShape
should only have methods that don’t care about dimension, and operations that care about dimension should be handled by other interfaces.
Your shapes shouldn’t hold their values through the interface. The interfaces are things you need to be able to do with them. The classes are the various ways you can fulfill those same things.
If your effort is to draw these shapes to the screen your interface is going to have nothing to do with values of any kind directly.
public interface IDrawable {
void Draw(Graphics g);
}
This way you can invent completely new shape ideas you never thought of without having to change the signatures of how they interact. That is all the OO is for. Isolate the interaction to a single thing that will work for everything. (hint: You will often be abstracting a function, not a value.)
Now when you decide you need to be able to write these shapes to a data storage you can add.
public interface IWritable {
void Write(Stream s);
}
Now there is place for these values to be typed, everything works better if its typed. Notice you can pretty much put anything here now, and freely mix-and-match.
public class Line : IDrawable , IWritable {
public Point Start { get; set; }
public Point End { get; set; }
public void Draw(Graphics g) { }
public void Write(Stream s) { }
}
public class ThreePointTriangle : IDrawable , IWritable {
public Point First { get; set; }
public Point Second { get; set; }
public Point Third { get; set; }
public void Draw(Graphics g) { }
public void Write(Stream s) { }
}
public class SinRightTriangle : IDrawable , IWritable {
public Point AngleA { get; set; }
public Line Opposite { get; set; }
public Line Hypotenuse { get; set; }
public void Draw(Graphics g) { }
public void Write(Stream s) { }
}
public class Car : IDrawable {
public void Draw(Graphics g) { }
}
The answer is simply, these aren’t similar objects. Inheritance is about shared behaviour, not shared values. Just because these objects are made up of points doesn’t mean they are related in terms of behaviour.
Leaving aside that getters and setters aren’t really behaviour and should be avoided if at all possible*, even in this case the getters and setters are doing different things, as hinted by the different signatures. This should make it clear, these objects do not share behaviour beyond the most meaningless sense of storing points.
(this is a good example of why to avoid getters and setters, as they can cloud the true behaviour of an object, and make it appear that unrelated objects are related because they all have getter and setter methods)
How about something like this:
public interface ICoordinateValue {}
public class Value1D : ICoordinateValue {}
public class Value2D : ICoordinateValue {}
public class Value3D : ICoordinateValue {}
public interface IShape
{
IEnumerable<ICoordinateValue> GetValues();
void AddValue(ICoordinateValue value);
}
2