Subscribe to get more articles about programming languages
Donate

Class extensions sugar violates SRP

ugly building with satellite dishes

What are class extensions?

In many popular object oriented programming languages classes and methods are declared upfront, and each class is defined in its own file (sometimes a pair of connected files). Then that file is wrapped in some packaging unit (such as a library, a package, DLL), and that’s it, the class structure becomes final… or not?

Class extensions is a superpower: it is a programming language feature that lets you extend any existing class (usually by adding new methods). The extension can reside in a separate file, even a separate library.

Example in Objective-C

// file Cable.h
@interface Cable : NSObject
- (void)plug:(id<Device>)device;
@end

// file Cable+CableExtension.h
// Cable class extension declaration
@interface Cable (CableExtension)
- (void)plugMany:(NSArray<id<Device>> *)devices;
@end

// file Cable+CableExtension.m
// Cable class extension definition
@implementation Cable (CableExtension)
- (void)plugMany:(NSArray<id<Device>> *)devices {
    for (id<Device> device in devices)
        [self plug:device];
}
@end

// usage
NSArray<id<Device>> *devices = @[ [Mouse new], [Keyboard new] ];
Cable *cable = [Cable new];
[cable plugMany:devices];

Example in Swift

// file Cable.swift
class Cable {
    func plug(_ device: Device) { /* ... */ }
}

// file CableExtension.swift
extension Cable {
    func plugMany(_ devices: [Device]) {
        for device in devices {
            self.plug(device)
        }
    }
}

// usage
let devices: [Device] = [ Mouse(), Keyboard() ]
let cable = Cable()
cable.plugMany(devices)

Example in C#

// file Cable.cs
class Cable {
    public void Plug(IDevice device) { /* ... */ }
}

// file CableExtension.cs
static class CableExtension {
    public static void PlugMany(this Cable cable,
            IEnumerable<IDevice> devices) {
        foreach (IDevice device in devices)
            cable.Plug(device);
    }
}

// usage
IDevice[] devices = { new Mouse(), new Keyboard() };
var cable = new Cable();
cable.PlugMany(devices);

This feature existed for many years in popular compiled mainstream languages like C# and Objective-C (and now Swift). It also can be used in dynamic languages (such as Ruby/Python/JavaScript) where this technique could be referred to as monkey patching.

It is a syntax sugar construct that is often abused in the codebases.

Abuse cases

⛔️ God mode

The class extensions feature makes people feel godlike and they extend the standard library classes (especially things like String, Array/List, Url) with the application’s business domain concerns. This is bad because it violates SRP and brings confusion for the code readers. It becomes hard to say if a method is standard or it was slapped in by a mastermind.

⛔️ Pure convenience for the author

Adding methods for the sole reason of convenient local access is a bad idea. Yes, it is nice to see a list of methods pop up after typing “.” in an IDE (like self.someObject.), but if you do this without thinking, you probably violate SRP again, and the code becomes a mess. Instead it might make sense to consider adding that method to the class directly, or otherwise - maybe creating a new class is better? Convenience alone should not be a sufficient argument to violate the original author’s design.

⚠️ Extension methods that never utilize self (aka this) is a code smell!

Nevertheless the class extensions feature has a few good uses.

Good use cases

✅ Adapt library classes (protocol interface adoption)

Let’s say you have an object of type T from library L1, and a method M that accepts interface I from library L2. Conceptually T and I are similar or compatible, but not out of the box. T doesn’t conform to I, so some adaptation is needed. This is where the adapter pattern might be applied as well. Class extensions give you the power to force T to be compatible with I and make the method M happy.

✅ Generated code

You want to generate some boilerplate, or some methods variants that are based on the methods in the primary class. This way your main class file is separate from the generated code. You are able to regenerate it when needed without affecting hand-written code. Neat.

✅ Sealed outdated library

You are stuck with some old version of a library, but you want to use a new method from that library. Sometimes it is possible to implement this new useful method (or a method overload) yourself. This is a case for polyfill type of extensions.

✅ Standard libraries

Imagine that the thing you work on is a very popular library with a massive class that is already too big to extend in a usual way. The core class is hard to change, it needs to be stable and solid, and you have to keep it backward compatible. In this case while the primary team produces the core class, another team might develop a set of optional extensions. This is a case for things like LINQ extensions or Reactive extensions.

Conclusion

The abuse cases are an easy trap to fall into, while good use cases are precise and specific. Think twice before using class extensions again. If you are able to do something - should you be doing it?

Subscribe to get more articles about programming languages
Donate