1

Edit: I'm copying the question but changing the example code. Apparently, I used a bad example earlier that contained an imprue getter. I'm keeping the old example code at the bottom so the first answers stay relevant.

Is storing computed values in the app runtime state always bad in OOP? Assuming that:

  1. Computation cost is low or neglectable.
  2. The object representing the state is immutable but many instances will be created from various parts of the code.

For example, is this code bad, and should be written as the next code block?

// bad
class AppState {

  AppState(int debt, int savings, int netWorht);

  final int debt;
  final int savings;
  final int netWorth;
}
// good
class AppState {

  AppState(int debt, int savings);

  final int debt;
  final int savings;
  int get netWorth => savings - debt;
}

If you think that the answer is yes (the first code block is bad), please share any resources you may have. I do believe so too but couldn't find any resource to back up my argument.

The old example for reference

// bad
class AppState {

  AppState(Date birthday, int age);

  final Date birthday;
  final int age;
}
// good
class AppState {

  AppState(Date birthday);

  final Date birthday;
  int get age => DateTime.now - birthday;
}
4
  • I would suggest revising your example to not include DateTime.now as that does not have referential transparency, thus making your object not immutable in many senses of the term. Sep 26 at 22:24
  • Thank you! You're right, my example was bad and distracting from the problem. I edited the question, hope it's to the point now. Sep 26 at 22:36
  • 1
    The issue that I have with "AppState(int debt, int savings, int netWorht);" is that networth may not have to be (savings-debt) but a totally independent value.
    – Pieter B
    Sep 27 at 10:57
  • To the point that @PieterB makes consider the situation where the parameters to your first example are (0, 0, 1000000) respectively. There's no reason to take a value as a parameter when it is calculated from other parameters. Whether you should store the result is a different question. In theory, since debt and savings are immutable, the compiler can optimize and store the result automatically.
    – JimmyJames
    Sep 27 at 20:44

5 Answers 5

2

The guiding principal here is DRY (don't repeat yourself) during object construction. The second case doesn't require anyone constructing the object to repeat the calculation savings - debt.

If the calculation was more logically complex (even if not CPU or memory intensive), for example referencing many different objects to calculate a value you may choose to use the first example (store the final value), in this case I would recommend using a factory pattern to avoid violating DRY (all the complex logic would be encapsulated in the factory object/method).

It's difficult for me to say that in this extremely trivial example there is a valid reason to use the first example.

However it doesn't take much more complexity than this, for me to consider making all constructors private and adding a static factory method to the class - that prevents the DRY violation.

Doing that can make the construction / overall logic both more readable and debug-able since all the calculated values are visible when inspecting the object in a debugger and/or can be logged/printed during construction.

TL;DR - Violating DRY is generally bad (with exceptions), however I can't say that a more generalized form of either of your examples is good or bad - it depends on the case case.

1
  • Yep! Factory pattern is the answer here. It's DRY, efficient, and prohibits others from creating/copying the state with their own implementation for the calculated fields. Thank you. Sep 27 at 10:38
3

"age" is of course not just a computed value. It is a computed value that is (very slightly) different on each call, so you really really don't want to store the value. Except if a calculation started half a second before midnight just before my birthday; you don't want to change the number of years in the middle of your calculation if the calculation takes more than half a second.

If you store "date of birth" and "18th birthday" you just might store the 18th birthday, but there are good reasons not to. The biggest is: What if you read both values and they are not consistent? One of them is wrong, but which one? And checking for consistency would be stupid in the first place, because just calculating the 18th birthday would be much easier.

1
  • Thank you. I should have avoided the impure getter, and, I guess, dealing with dates in general. I edited the example. Would like to hear your opinion now. Sep 26 at 22:38
3

To make a true apples-to-apples comparison, modify the first version to make it semantically equivalent to the second, i.e. the class should encapsulate the calculation and not allow any outside to supply incorrect values.

class AppState {
  AppState(int debt, int savings) {
    this.debt = debt;
    this.savings = savings;
    this.netWorth = savings - debt;
  }

  final int debt;
  final int savings;
  final int netWorth;
}

This gets rid of the DRY issue the accepted answer notices.

At this point, this is simply called "eager computation" and is an engineering trade-off compared to the "lazy computation" approach of the second example. There is no correct general answer; instead you must look at the two versions and decide which one fits your particular needs better.

Eager computation: needs additional memory to store the value, might do unnecessary work if the value isn't always used, saves time if the value is used a lot (where the relevant value of "a lot" depends on the complexity of the computation).

Lazy computation: saves memory, which could even save time if the computation is trivial and you get better cache locality, saves even more time if the value isn't even accessed for every object created, loses time for repeated computation if the value is accessed a lot.

Bottom line: lazy computation will often be better unless the computation is expensive. But you still have to decide on a case-by-case basis.

0

Both examples are the same...

class AppState {

  AppState(int debt, int savings, int netWorht);

  final int debt;
  final int savings;
  final int netWorth;
}
class AppState {

  AppState(int debt, int savings);

  final int debt;
  final int savings;
  int get netWorth => savings - debt;
}

...with the slight difference that the second example computes the netWorth with every call though the returned value it is the same with every call since debt and savings are immutable.

AppState from the examples is a snapshot of the state at the call time, for improved efficiency the version with three constructor parameters AppState(int debt, int savings, int netWorht); is preferred though the version with two constructor parameters AppState(int debt, int savings); could be used with equal efficiency adding some boiler plate code that uses the AppState to store the returned value of netWorth in a variable used further.

An example relevant for a dynamically computed value is the concept of elapsed time, that is having a state encapsulating a reference time and compute the elapsed time with every call, although there are solution to implement it both ways (1) retrieve the state encapsulating the elapsed time with every call (2) retrieve the state once and compute the elapsed time with every call.

To use the right tool for the job get specific.

1
  • Thank you for the answer. You focused on efficiency. However, I'm not concerned about performance at all in this case. As I mentioned in the question, <Computation cost is neglectable>. I'm only concerned about the cleaners and reliability of using this class as an API. Sep 27 at 10:33
0

Just in case you have a calculated value where the calculation is expensive, and done repeatedly. So expensive that you favour speed over code complexity:

Create a backing variable with the last calculated value.

Create either a Boolean indicating that all inputs to your calculation are unchanged, or copies of all inputs. In the first case, any setter for these values must mark your input as “invalid”.

Your getter checks whether all inputs are still valid, or whether they are unchanged. In that case return the last value, otherwise recalculate.

Not the answer you're looking for? Browse other questions tagged or ask your own question.