Adding functionality to classes
tl;dr
Before mindlessly changing an existing class, carefully think about the responsibility that the class should have. Use these guidelines for software design depeding on your change:
- Specializing a class - Create a new class, use composition.
- Extending a class - Create a new class, use inheritance.
- A bit of both - Break out common functionality, create new classes.
Responding to change
Often you will want an existing class to do more. Maybe you are working on a change or feature which was not known when the class was made.
The easy (but often bad) way out is to just change the existing class. But just adding to the class will lead to bloat.
The practice of mindlessly adding functions or fields to an existing class will inevitably lead to bloat.
A software design mindset
It is important to follow the Single Responsibility Principle of SOLID. That could be summarized as:
"A class should do one thing and do it well"
In other words, you need to look at the role of the class. How would your desired change affect the class' responsiblity?
Let's say you are creating a UI-component called Heading
, responsible for displaying a large title text. Now, your desired change is for some headings to have icons. Adding an icon-field to the Heading
-class will dilute its responsibilities! An alternative approach would be to create a new specialized class HeadingWithIcon
.
Specialization
If the desired change is to define some default behavior, i.e: specialize an existing class: use composition.
For instance, let's say you have a Logger
class that can print messages. Now you want some logs to be prefixed with their severity (warning, info, error, etc.):
WARNING: Got a null value ERROR: Ice-cream delivery sub-process crashed! INFO: Ice-cream delivery will be late.
Instead of changing the existing Logger
, create a new specialized class - SeverityLogger
public class SeverityLogger {
Logger _logger;
public SeverityLogger(Logger logger) {
_logger = logger;
}
public void Log(string message, Severity severity) {
string sevStr = SeverityToString(severity);
_logger.Log(sevStr + ": " + message, severity);
}
private string SeverityToString(Severity s) {
return (
s == Severity.WARNING ? "WARNING" :
s == Severity.ERROR ? "ERROR" :
s == Severity.INFO ? "INFO" :
"UNKNOWN SEVERITY";
}
}
Extension
If the desired change is to optionally add some extra data to an existing class, then extension might be a good choice (notice how "add" is in opposite to "specialize").
Let's say you have a menu structure set up, consiting of multiple MenuItem
. Now, you would want to hide some items depending on the user's access level.
Instead of changing the existing MenuItem
, create a new class AuthorizedMenuItem
that extends MenuItem
:
public class AuthorizedMenuItem : MenuItem {
int _accessLevel;
public AuthoriedMenuItem(int accessLevel, ...) {
super(...);
_accessLevel = accessLevel;
}
public boolean CanBeViewed(AccessLevel userAccessLevel) {
return userAccessLevel >= _accessLevel;
}
}
It should be noted that this use case is perhaps even more suited for decorators or instance meta-data (if your language supports it).
Difficult cases
Let's face it: you are likely to run into situations where you want a bit of specification AND some new functionality. In this case, refactoring is your best friend.
Break out important common functionality into smaller classes or functions.
Some code repetition is bound to happen, and that is ok.
Let's say you have a simple Button
-class:
public class OldButton {
public void SetOnClick(Action<>);
public void SetText(string);
public void Draw();
}
Now you sometimes want the button to display an icon as well as text. In this case, you could try to break out the outline and text-settings into its own class:
/**
* Superclass for drawing empty buttons
*/
public class BlankButton {
public void SetOnClick(Action<>);
public void Draw(); // Draw the outline and call DrawContents()
// Open up for subclasses to do their own thing
public virtual void DrawContents();
}
/**
* Class for drawing a button with a text label
*/
public class TextButton : BlankButton {
string _text;
public TextButton(string text) {
_text = text;
}
public override void DrawContents() {
someDrawApi.drawText(_text);
}
}
/**
* Class for drawing a button with a text label
* and an image icon
*/
public class TextIconButton : BlankButton {
string _text;
Icon _icon;
public TextIconButton(string text, Icon icon) {
_text = text;
_icon = icon;
}
public override void DrawContents() {
someDrawApi.drawText(_text);
someDrawApi.drawImage(_icon);
}
}
The power of interfaces
The drawback of using these methods (except extension) is that you can't use your new classes as drop-in replacements in existing code. If you existing code expects a Logger
, you can't just provide a SeverityLogger
without changing all the places using the Logger
.
This is a great reason to try and use interfaces in your code from the start. If your code is depending on an interface ILogger
, it is quite simple to make SeverityLogger
confirm to that interface:
public interface ILogger {
Log(string message, Severity severity);
}
public class SeverityLogger : ILogger {
// ...
public void Log(string message, Severity severity) {
string sevStr = SeverityToString(severity);
_logger.Log(sevStr + ": " + message, severity);
}
}
pubic class Application {
ILogger _logger;
public void Main() {
_logger = new SeverityLogger(new ConsoleLogger());
// ...aaand the rest of the application code can be unchanged :)
}
}
Don't be an idiot
As stated in the beginning, carefully consider the desired responsibility of the class. Don't just go and implement a slew of nice design patterns just because you can.
Sometimes it makes perfect sense to straight up add functionality to an existing class. However, that decision should be dictated by software design, and not the need for change itself!