DEV Community

Jules Roadknight
Jules Roadknight

Posted on

On the Open-closed principle (Decoupling & Abstraction)

Here, I'll be tracking the abstraction of a Contact Manager that I've been decoupling, similar to the example in my post on Interfaces. The aim of this is to reach a point when I have a core program that needs no modification to add new functionality, but instead can be extended with new classes, per the second of the SOLID principles.

As a program grows, it's important to keep an eye on its structure and spot opportunities for abstraction.
If a class gains a few more functions, you might find, for example, that the single mention of the string name of a field becomes a regular reference, and that it's worth promoting a string to a variable, or a series of constants to a class.

Unless it's important, I'll use abstract methods with no bodies. Below is the Contact class from my program, which stores data and modifies it.

Example 1 - Contact

public class Contact {

public String FirstName;
// plus five more fields

public Contact(String firstName // five more here too) {
   this.FirstName = firstName;
    // plus five more fields
}

public updateFirstName(firstName) {
    if (!firstName.matches(String.valueOf(ValidateInput.blankString))) {
        FirstName = firstName;
    }
}
// plus five more almost identical update methods
}

Immediately there are a couple of problems here. The first is that I've got six methods doing the same thing with only one variable.

Abstraction Concept: Repetition indicates that there's room for abstraction, so let's generalise these methods.

The second is that the Contact class is responsible for both storing and modifying its own state. That's a lot of responsibility for what should be the computer equivalent of a card with six lines on it.

It also means that the 'structure' of the program is really just a series of inseparable classes, as ContactManager relies on Contact when it tells it to update itself.

Abstract-ification Sequence

public class ContactManager {

private ConsoleIO consoleIO;
private ArrayList<Contact> contactList;

public ContactManager(ConsoleIO consoleIO, ArrayList<Contact> contactList) {
   this.consoleIO = consoleIO;
   this.contactList = contactList;
}

public void updateField(String value, Contact contact, int field) {
    if (!value.matches(String.valueOf(ValidateInput.blankString))) {
        switch (field) {
            case 1: contact.FirstName = value; break;
            case 2: contact.LastName = value; break;
            case 3: contact.Address = value; break;
            case 4: contact.PhoneNumber = value; break;
            case 5: contact.DOB = value; break;
            default: contact.Email = value; break;
        }
    }
}
// and a lot of other methods
}

And now Contact is just

public class Contact {

public String FirstName;
// same as before, 5 more

public Contact(String firstName) {
   this.FirstName = firstName;
}
}

Much better. Now six methods are one, and the Contact class is 18 lines long, with the sole responsibility of storing information.

Example 2 - ConsoleIO

Note: If you don't know about interfaces, scroll back up and see the other post in my Abstraction series.

ConsoleIO is my class for handling input and output, specifically in the console.

To implement the InputOutput interface I made, ConsoleIO needed four methods:

  1. display
  2. getStringInput
  3. getMenuInput
  4. confirmInput

My ConsoleIO class had the required four methods to implement the InputOutput interface, but it also had methods that validated the input.

public class ConsoleIO implements InputOutput{

public ConsoleIO()

public void display()

public String getStringInput()

public int getMenuInput()

public String getInput()

public static Boolean validateInput()

public static Boolean validName()

public static Boolean validNumber()

public static Boolean validDOB()

public static Boolean validEmail()

public static Boolean isBlank()
}

What had started as a method or two grew to six, and this class never really needed to know the rules for valid input. If this were a whiteboard, I'd draw a big circle around everything with valid in it (and that last one too, close enough).

Abstractionise

public class ConsoleIO implements InputOutput{

public ConsoleIO()

public void display(String message)

public String getStringInput()

public int getMenuInput()

public String getInput()
    // this method calls validation methods
}

ConsoleIO is back down to its core methods, and below we have a new class that doesn't even need to be initialised, since everything's static.

public class ValidateInput {

public final static Pattern blankString = Pattern.compile("^$");

public static Boolean validateInput(){
    // this one calls the ones below as appropriate
}

public static Boolean validName()

public static Boolean validNumber()

public static Boolean validDOB()

public static Boolean validEmail()

public static Boolean isBlank()

}

Decoupling

It's getting a lot easier to look at these classes and tell what they do. ConsoleIO doesn't know what makes input valid, and ValidateInput doesn't know where the input comes from or goes back to.

Decoupling Concept: If a class isn't designed to handle other classes (like ContactManager handles Contacts), then it should know nothing about how it works. The less it knows, the less it relies on low-level details.
Passing information from ConsoleIO to ValidateInput should be seen as a black-box, where input goes in, mysterious (see: abstract) doings occur, and then output comes out.

It might seem that this isn't much of an improvement; that I've just moved methods from class 1 to class 2 and that class 1 still needs the methods of class 2. However, the value comes in that I can swap ConsoleIO for a different InputOutput interface-based class like 'WebIO', and I don't need to repeat the validation methods.

Abstraction Concept: Abstraction doesn't have to make a difference now, the value comes in facilitating future change. I've made room for future classes, but I don't need to know how they work.

End Goal

While this program isn't finished, the examples I gave were basic changes that made incremental improvements now, and make future change much easier.

The aim of this program is to reach a point where each class only knows just enough to do its part, so that I could swap out any class with another that follows an appropriate interface.

What this achieves is maximum room for extension, without needing to anticipate any specific functionality. Using this on the web? Swap out ConsoleIO with the new WebIO.

Discussion (0)