Home > Blockchain >  When we change the implementation of a method, do we have to recompile dependent classes?
When we change the implementation of a method, do we have to recompile dependent classes?

Time:01-23

Let's say that we have the following method in class TaxCalculator:

public double calculateTax(double income) {
    return income * 0.3;
}

And we use this method in the Main class like this:

var calculator = new TaxCalculator();
double tax = calculator.calculateTax(100_000);
System.out.println(tax);

If I change the implementation of the calculateTax method to:

public double calculateTax(double income) {
    return income * 0.4;
}

Do I need to recompile both the TaxCalculator class and the Main class?

I know this question sounds very silly, but I heard in a lecture that if we don't use interfaces, every change we make in tightly-coupled code (like the one I showed above) will force us to recompile all the classes that depends on the class we made the change.

And this sounds weird to me, because the Main class doesn't know the implementation of the method we've made the change.

Thanks in advance for any help!

CodePudding user response:

Yeah, that lecturer was just dead wrong. More generally, this is a very outdated notion that used to be a somewhat common refrain, and various lecturers still espouse it:

The idea that, if you have a class, you make a tandem interface that contains every (public) method in that class; given that the class already occupies the name of the concept, the interface can't be given a good name, and thus, an I is prefixed. You have a class Student and a matching interface IStudent.

Don't do that. It's a ton of typing, even if you use tools to auto-generate it, everytime you change one you are then forced to change the other, and there is no point to it.

There are exotic and mostly irrelevant ways in which you get a 'more tight coupling' between a user of the Student class and the Student class code itself vs. having that user use IStudent instead. It sounds like either you or more likely perhaps the lecturer is confused and presumed that this tight coupling implies that any change in Student.java would thus require a recompile.

Furthermore, if those examples are from the lecture, oh boy. double is absolutely not at all acceptable for financial anything. That should most likely be an int or long, representing cents (or whatever passes for 'atomic monetary unit' for the currency in question; pennies for pounds, satoshis for bitcoin, yen for yen, and so on). In rare cases, BigDecimal. In any case, not, ever, double or float.

You need to recompile B, where B uses something from A, if:

  • You change the value of a constant in A that is used directly used by B, and that constant was 'CTC' (Compile Time Constant). Only the primitives and strings can be CTC, and they are CTC if the field is static final, and is immediately initialized (vs. initialized in a separate static {} block), and whose expression is itself CTC, which means its comprised of literals and possibly simple operations between CTCs, e.g. in static final int a = 5; static final int b = a 10;, b is also CTC. In contrast, e.g. static final long c = System.currentTimeMillis(); is not a compile time constant because System.currentTimeMillis() isn't, for obvious reasons.

  • You change a signature of any element in A that B uses. Even if the caller (B.java here) can be recompiled with zero changes. For example, you have in A.java: void foo(String param) and you change that void foo(Object param). Even though foo("hello") is a valid call to either method, you still need to recompile here. Relevant elements are the name of the method, the types of the parameters (not the names), and the return type. Changing the exceptions you throw is fine. Deleting something in A that B used is, naturally, also something that'd require a recompile.

And that's essentially it. The interjection of an interface doesn't meaningfully change this list - if that constant is in the interface, the same principle applies (if you change it, you have to recompile users of this constant), and if you change signatures, you'd have to change them in the interface as well, and we're back where we started.

Adding an interface does have some generally irrelevant bonuses.

  • As a caveat, any such attempt must always answer the rather poignant question of: But how do callers make an instance? If the lecturer uses IStudent student = new Student(); they messed that up, and the few mostly irrelevant benefits of using an interface are gone.

  • If there are meaningfully different implementations available (quick rule of thumb: If you can come up with good news for all relevant types, this is the case), using an interface is 'correct' and none of this applies. For example, java.util.List is the interface, java.util.LinkedList and java.util.ArrayList are meaningfully different implementations of the same idea.

  • It's slightly easier to make an implementation of the interface specifically for test purposes. However, mocks and extending the class are generalized solutions to this problem too and usually work just as well, and more generally making a test-specific impl requires more care than just a rote application of the 'make a mirroring interface' principle.

  • You get an extra level of access - you can have public things in the class that nevertheless aren't mirrored in the interface, and thus, are not 'accessible' via the interface. There is a single good reason to make things public when they aren't really meant for external consumption: When you have a multi-package system. Java's module system acknowledges this too, and (via the 'exported package' concept) also introduces, effectively, another access level (a public thing in a non-exported package is not accessible from other modules, it's not as public as a public thing in an exported package). This is outdated, and there are ways around it even in a multi-package library, and it doesn't actually stop much - you cannot enforce callers to 'code to the interface'1

  1. Well, you can, but those are a bit clunky, and those would also stop another package in the same project, which was the whole point. You can use hacks to get around this, but if you're willing to use these hacks, you can just make those public, but not actually meant for external consumption non-public and use the same hackery.
  • Related