Let's say library A, B and C are all custom math libraries. They may or may not use the same data types. The libraries have methods that accept data type inputs like ProcessAsync(DataTypeA)
or ClusterByFeature(IDataTypeB, Func<IDataTypeB, double>)
.
These libraries will be developed for modularity. E.g. Future graduate students can come and use the library for their needs.
I thought that these libraries should:
- either provide concrete implementations of the inputs they accept and manipulate on, like
DataTypeA
,DataTypeB
,DataTypeC
- or, request that the users supply an object of type
IDataTypeA
,IDataTypeB
,IDataTypeC
Which design I should choose seems to be dependent on many factors.
I've come up with a few:
- If it's just a container, it should be an interface
- If
DataTypeA
,DataTypeB
,DataTypeC
are all very similar, use interfaces - let the invoking framework supply the implementation by inheriting all the interfaces - If
DataTypeA
requires complicated operations, use classes - If
DataTypeA
requires simple operations, use interfaces and aDataTypeAManipulator
class
I see other open-source libraries overwhelmingly use classes instead of interfaces, so I think classes should be the way to go. But is there no place for data interfaces then?
--
A better example of the problem that I faced: There's a 3D object that needs to be manipulated on. Library A handles vertices, Library B handles vertices and deformation by bone joints, Library C handles vertices and texture mapping. 3 different data objects - they're actually different views of a single data object, but the libraries don't/shouldn't know that! So should I use data interfaces to show different perspectives of a 3D object, or copy values to data classes instead.
1 Answer 1
Data classes make sense when they are used as a container for data (a POCO in .NET or POJO in Java). They contain no behaviour and simply serve to encapsulate data and simplify code, eg:
int CalculateSurface(int x1, int y1, int x2, int y2)
vs
int CalculateSurface(Rect area)
When you pass a data class, you could actually write the method so as to take the arguments separately. You obviously don't WANT to do that because it seriously clutters up your code, but you could.
A data interface makes sense when you actually need logic or behaviour, which means methods on the passed object need to be implemented. It also makes sense when you want the object you are working on to perform some operation and you need to pass it a parameter, in which case you need a contract for this method, hence an interface.
-
So by "when you actually need logic or behaviour", you're referring to behaviour that the user has to define, like IEnumerable.MoveNext() etc.? In other cases where the library knows how to perform this logic, I assume it's fine to still use a data class that has instance methods.Cardin– Cardin10/15/2015 09:00:02Commented Oct 15, 2015 at 9:00
-
And by extension, I suppose if it's just a data container, tuples/structs would work fine too as a data class..Cardin– Cardin10/15/2015 09:00:55Commented Oct 15, 2015 at 9:00
-
2Good answer. Just one comment. In my experience combining data and logic in the same class tends to get you into a world of trouble. This is because logic you may want to reuse can easily get tightly coupled with the data class it was first invented for. Having logic in separate classes reduces this risk significantly and makes for easier to understand code.Jonathan van de Veen– Jonathan van de Veen10/15/2015 09:19:42Commented Oct 15, 2015 at 9:19
-
@JonathanvandeVeen, it simply isn't possible to upvote your comment enough. "combining data and logic in the same class tends to get you into a world of trouble". Every OO developer ever needs to have this drummed into them until they bleed.David Arno– David Arno10/15/2015 09:37:52Commented Oct 15, 2015 at 9:37
-
@JonathanvandeVeen Assuming the rules above still apply, then to avoid users from repurposing the data class beyond its original use, it can be marked as final/uninheritable. Or if you meant data interfaces with data and logic, then maybe 1 data object interface and 1 data manipulator interface.Cardin– Cardin10/15/2015 09:39:38Commented Oct 15, 2015 at 9:39
ClusterByFeature(IDataTypeB, Func<IDataTypeB, double>)
the user defined function gets passed the interface. It benefit from receiving the concrete type instead. But you didn't specify enough context to really analyze your problem.