Let's say I have a class with some suitable instance variables which are then used for some computation (via a method of the class). As the computation will generate lots of data which needs to be accessed by other classes, it needs to be stored somewhere. What is the best way in this scenario? Obviously you can store the result inside the class but that couples logic and data. Another option might be generate separate data class but I am not sure if that is a bad design.
A key principle of software engineering is separation of concerns. This leads to separate independent things, and group strongly related things. Your question seems to be about that.
Your wording unfortunately leaves a couple of open issues regarding the context. Moreover its terminology could mislead. So, let's first agree that classes and objects are meant to couple and encapsulate data and processing. So "coupling data and logic" is not an issue but simply a fact of object oriented life. Hence, what your use of the word "data" is to be understood as a colloquial way to refer about the information value of objects (which could be of basic types, or of a more complex class composing several types).
If we ignore the ambiguous wording we then have the following main options:
Case 1: The complex computation completely belongs to the responsibility of the class on which it is performed. You could then perfectly store the result of the computation in the class. In fact, this is the foundation of memoization.
Example: The objects of a class
Polygonare made of a sequence of many
Point. The calculation of the Polygon's surface is a complex calculation that needs to identify a lot of triangles, and sum up the surface of all these triangles. The calculation of the surface is probably a responsibility of the
Polygon. Other classes may access the results using the method
calculateSurface()which might store internally in private properties the results to reuse them in subsequent calls and avoid to recalculate everytime.
Case 2: The complex computation is independent of the object on which it is performed, but the result makes only sense in the context of the computation. You'd prefer to create a separate object for the independent computation with its results. This decouples the algorithm and its results, from the input class.
Example: a general-purpose statistical algorithm such as a linear regression can require time-consuming computations. You may want to use this algorithm on the points of the polygon. You'd therefore design a generic
LinearRegressionclass, in which you inject an iterator that will access the input points. This ensures that the same algorithm could be reused also for real-time quotations on the stock market. You'd probably use an adapter that extracts the points from the polygon. The results would then be stored in `LinearRegression. This approach is also useful if it's not about one algorithm but a family of algorithms sharing some intermediary computations.
Case 3: Only the result of the computation matters, and it has a life on its own. THen, regardless of how you'd implement the computation, a result object should be created.
Example: the input objects are pictures of a VTC camera. A complex image processing AI algorithm extracts a set of polygon of the picture. Each polygon corresponds to a human face identified in the picture. The polygons produced are then used independently of the original image, which could even be discarded, for example to find a match in a face-recognition database, or to compare faces identified accross several images.
So the most suitable approach depends heavily on the context, and especially how much the results are bound to the input or the algoirthm itself.
I suspect the real problem here is that there exists a point in time when you know the instance vars but you haven’t yet done the expensive calculation to get the data. Which means if you store the data in the object it now has two states: before and after. Bleh.
I think it’s better to have an object that knows the logic and vars used to make the data but that shoves the data into a new object that it hands to whatever asked for it.
This way there isn’t two states. There’s just an object that makes the data object when asked. Done this way it can immutable. It’s ready to make the data when it exists. And the data object is ready to be used when it exists.
What’s really nice is you don’t have to ask things if they’re ready to be used because if they exist, they’re ready.
If the problem is that you need the data in contexts where you don’t remember if it’s already been created you can make things more efficient by caching the data. Having immutable objects hold it doesn’t stop you from doing that. Memoization works with them just fine. We do with immutable Strings after all.
But sometimes the best way to solve a problem is to avoid it. If you need the data then let something inject it into you. Don’t go asking for it.