Wednesday, September 26, 2018

Redundancy vs. Duplication Examination - Method Calls

Previously, I wrote about redundancy and how it differs from duplication.

I'd like to dig into that situation a little with an example. Imagine a design where one class provides a method used by many others.

A UML Static Structure diagram of four classes. One is named "Provider" with a method named "Provide". One class is named "Consumer1" with a method named "UseProvided" and an has-a relationship to Provider. One is named "Consumer2" with a method named "Use" and a has-a relationship with Provider. The final class is named "Consumer3" with a dependency on Provider caused by a method named "UseIt" that takes a Provider as a parameter.
A basic static structure diagram

In all cases, the name of the method and certain expectations about its signature are replicated throughout the system. That’s not really a problem, though. In fact, it makes a problem go away: wherever that method call exists, there could just be a pile of duplicated code, instead.

If we’re talking about a compiled language and static types, this is a no-brainer. If you need to change the signature, there are probably tools to do it for you. Even if the tools don’t work or you choose not to use them, the compiler will catch most of the mistakes you’re likely to make.

the exact same image as the previous diagram with a red note that says "compilers force changes to travel along these lines" with arrows pointing to the three relationships.
If you use a compiler, method calls don't count as redundancy

If we’re talking about a non-compiled language or a dynamically-typed system of classes, the duplication becomes an issue. The purpose of a method signature is to serve as a contract between two objects. If that contract changes, it needs to be changed at all touch-points.

You can get around that problem by changing how you change things or changing how you find things. For instance, you can rely more-heavily on integration tests to make sure every combination of objects that might need to work together can do so successfully.

The same as the initial diagram but with an additional class that tests Comsumer3 and a red overlay that shows how the coupling between Consumer3 and Provider can be protected with a test.
Integration tests can do part of the job of a compiler

You might also rely more heavily on design, encapsulating intentions into their own classes and methods and decoupling the expression of intent from its fulfillment. This would mean that changing a signature (from the caller's perspective) would not need to be a "find and fix" problem. It could just be a "change one thing" problem.

The same as the original diagram but with an extra class. All calls are routed through this class so that there are direct calls to Provider except through that class.
Encapsulating a relationship turns redundancy from not using a compiler into non-redundancy

If you are careful to rely upon that indirection throughout your tests, too, you can enforce that the design change is propagated to all implementations of an interface.

The point, here, is to drive home the fact that redundancy doesn't exist in the duplication of form. It's not even like you can say all replicated purposes are redundancy. In a compiled, statically-typed language, method calls all represent a replicated purpose that is nonredundant because the compiler finds them all for you. In a language without those protections, the exact same design suddenly represents redundancy.

The situation is part of deciding whether or not something is redundant.