Static method and classes are one of the OOP world’s drawbacks. I am not implying that you shouldn’t use it at all, but in long term I believe a source code full of static methods and classes add more burden into the maintenance process.
How does static code look like?
- Worker methods. Good for Simple calculations / processing, i.e
- Factory methods. Used to return preconfigured instances of a class, i.e.
- Singleton methods. Used to enforce a single global instance of a class, i.e.
- Global variables. Used to store configuration values, i.e.
** Do not confuse it with Factory design pattern!
Why do we prefer the easy way out?
Suppose we have two classes A and B, and have a method M() that both must use, then the most naive approach is to repeat the method in both classes. However, this violates the “Don’t repeat yourself” (DRY) approach. It’s not just about reducing work: if both classes truly need the same method, then it should be the same method.
The most natural solution is inheritance, but it’s not always beneficial for A and B to be sub classes of some parent class. The bad and easy alternative is to define a “Utility” class: a public static class that sits in the global namespace, awaiting anyone to “borrow” them.
Static classes and methods imply relationships between data that are not explicitly defined. Also, if the static classes have any static variables, then A and B have no idea which object called them.
Where do static methods belong?
A class in OOP has state. When we look at our classes from the Single Responsibility Principle (SRP) viewpoint, a static method is usually a violation because it tends to have a responsibility that is not the same of the class it is attached on. So it ends up sitting out there trying to belong to the class it is on, but it doesn’t really belong, because it doesn’t use the internal state of the class.
Furthermore, based again on SRP, a class should have one and only one reason to change. But if we end up designing huge utility classes that contain any method the developer could think of, (e.g. a class containing a helper method for URL encoding, a method for looking up a password, and a method for writing an update to the config file) this is crystal clear violation of the Single Responsibility Principle.
Static methods and the rest of S.O.L.I.D.
Liskov Substitution Principle. Derived classes must be substitut-able for their base classes — If a class has only static methods , can not have a derived class. Maybe it’s not a direct violation, but every time we loose, we loose more and more destroying the project’s architecture.
Interface Segregation Principle. Class interfaces should be fine-grained and client specific. Since static classes do not derive from an interface, it is difficult to apply this principle with any degree of separation from the Single Responsibility Principle.
The Open Closed Principle. Classes should be open for extension and closed for modification. We cannot extend a helper class. Since all methods are static, we cannot derive anything that extends from it. In addition, the code that uses it doesn’t create an object, so there is no way to create a child object that modifies any of the algorithms in a helper class.
They are all “unchangable”. As such, a helper class simply fails to provide one of the key aspects of object oriented design: the ability for the original developer to create a general answer, and for another developer to extend it, change it, make it more applicable. If we assume that we do not know everything, and that we may not be creating the “perfect” class for every person, then helper classes will be an anathema to we .
The Dependency Inversion Principle. Depend on abstractions, not concrete implementations. This is a simple and powerful principle that produces more testable code and better systems. If we minimize the coupling between a class and the classes that it depends upon, we produce code that can be used more flexibly, and reused more easily.
With static classes/methods we have a clear violation of DIP. A class like that, cannot participate in the Dependency Inversion Principle. It cannot derive from an interface, nor implement a base class. No one creates an object that can be extended with a static class.
Static code and architecture
What is inside static methods? Well no one knows, and that is the problem. Static code must not keep inside it any meaningful state to the project . It should only carry out calculations statements like Math.Abs(), or String.ToUppercase(). We give an input, it works on that, generates the output. That’s it!
But unfortunately, reality is different. People always want more, and end up hurting their projects. Static methods might end up being huge, with complex code in them, with state, and sometimes create and manipulate objects, thus the complexity of the application is increased. The more static methods there are, the more a programmer working in the application has to know where is what and what’s in there. And this is only part of the problem.
Another part is naming static classes and methods. A static method with the name CalculateHolidays which calendar satisfies? Gregorian you will say!And you are right… most of the times! But there are seven calendars in regular current use around the world.
They are the following:
- The Gregorian (Is used worldwide for business and legal reasons)
- The Chinese (The Chinese calendar is not used in China but is used in various countries of south east Asia, usually with local variations. For example the calendar used in Japan is a variation of the Chinese one. It is also used socially by ethnic Chinese around the world.)
- The Hebrew (The Hebrew calendar is used, of course, in Israel, as well as by Jews around the world for their religious observances)
- The Islamic (is used by Muslims around the world for setting the dates of religious celebrations)
- The Persian (Iran and Afghanistan)
- The Ethiopian (Ethiopia)
- The Balinese Pawukon (Bali).
So 7 static methods with the appropriate names might one say ! Wrong! We have enums, factory design pattern, we can’t just drop all of them away and being lazy. Plus, renaming or replacing the class containing static methods necessarily requires refactoring all references to it.
Another issue we must address is memory management. Referring to a static class, the class itself is guaranteed to be loaded and have all of the necessary fields inside instantiated before it is ever referenced with the code. Its constructor will only be called a single time. So, this class and methods will remain in memory for the lifetime of the application’s domain.
Static code and unit testing
Unit testing assumes that we can instantiate a piece of the application in isolation. During the instantiation we replace any dependencies with mocks/fakes/stubs. We prevent the execution of the normal code path and is how we achieve isolation of the class under test. With static code we can’t away from the normal path, we can’t replace the static code, because there are no objects to replace.
Also, sometimes static methods is a factory for creating other objects. In tests we rely on the fact replacing important dependencies with mocks. A caller of such a static factory is permanently bound to the concrete classes which the static factory method produced.
In unit testing, we intent to test the monkey and how it eats the banana. With the static code, we are forced to add in the act, the tree the monkey sits on, the plantains the banana grew, and even worse the jungle itself. In the end, this is not unit testing…
Maybe the solution is interfaces! Composition or aggregation of objects over inheritance! Both of them are fairly easy to understand, we can see composition in everyday life: a chair has legs, a wall is composed of bricks and mortar, and so on.
Inheritance is more of an abstraction. Though it is possible to mimic inheritance using composition in many situations, it is often unwieldy to do so. The purpose of composition is obvious: make wholes out of parts. The purpose of inheritance is a bit more complex because inheritance serves two purposes, semantics and mechanics.
Inheritance captures semantics (meaning) in a classification hierarchy (a taxonomy), arranging concepts from generalized to specialized, grouping related concepts in sub trees, and so on. The semantics of a class are mostly captured in its interface, the set of messages to which it responds, but a portion of the semantics also resides in the set of messages that the class sends.
When inheriting from a class, we are accepting responsibility for all of the messages that the super class sends on our behalf, not just the messages that it can receive. This makes the subclass more tightly coupled to its super class than it would be if it merely used an instance of the super class as a component instead of inheriting from it. Note that even in classes that don’t “do” much, the name of the class imparts significant semantic information about the domain to the developer.
Inheritance captures mechanics by encoding the representation of the data/state (fields) and behavior (methods) of a class and making it available for reuse and augmentation in sub classes. Mechanically, the subclass will inherit the implementation of the super class and thus also its interface.
The dual purpose of inheritance can cause more confusion. Many people think that “code reuse” is the primary purpose of inheritance, but that is not its only purpose. An overemphasis on reuse can lead to tragically flawed designs.