Wednesday, February 3, 2016

Degenerate Abstractions


What do you get when you remove all but one refined abstraction from the bridge pattern? I already looked into what happens when you only need one implementor. The Gang of Four book even included a discussion about one implementor, terming it a "degenerate case." What about the opposite?

The prime mover in the bridge pattern is given the seemingly vague name of "The Abstraction." There are two required characteristics for the abstraction. First, it needs to perform some action whose exact implementation can vary. Second, it needs to have subclasses—different specific types of the abstraction. These subclasses are called refined abstractions mostly because they had to call them something and "concrete abstraction" is a contradiction in terms.

The idea is that client code can use different refined abstractions with different implementations. Maybe the client chooses which implementation to use. Maybe the refined abstraction chooses. It doesn't matter. Each refined abstraction should work with each action implementation. Furthermore developers should be able to make changes to the implementation without requiring corresponding changes to the refined abstractions and vice versa.

When there is only one implementation, the Gang of Four book concludes that there is still value to the pattern. Even if only one implementation for the action is ever used, the separation of the implementation from abstraction still buys independent changes to each. So the same thing should be true of multiple implementations and a single abstraction, right?

Let's take a look at the web example that I have been using. The backend communication has two implementations, one to send messages over websockets and the other to send messages over plain-old HTTP. In Dart, this looks like:
abstract class Communication {
  void send(String message);
}

// Concrete Implementor 1
class HttpCommunication implements Communication {
  void send(message) {
    HttpRequest.postFormData('/status', {'message': message});
  }
}

// Concrete Implementor 2
class WebSocketCommunication implements Communication {
  WebSocket _socket;
  WebSocketCommunication() { _startSocket(); }

  _startSocket() async { /* ... */  }

  void send(message) {
    _socket.send("message=$message");
  }
}
My original intent (it's important that I use that specific word) is to have multiple Messenger refined abstractions will allow people to post status updates:
// Abstraction
abstract class Messenger {
  Communication comm;
  Messenger(this.comm);
  void updateStatus();
}
In this thought experiment, I am coding the web version while another team is building a mobile Messenger. The Messenger interface tells both teams how to build our classes. We will be communicating with some form of Communciation and we need an updateStatus() method to post messages over those communication channels.

But something happens. Just after we start, the company decides that it is web-only and fires the entire mobile team. Since I want to avoid premature generalization, I get rid of the Messenger interface and dump everything into WebMessenger:
// Degenerate Abstraction
class WebMessenger  {
  Communication comm;
  InputElement _messageElement;
  WebMessenger(this._messageElement) : comm = new HttpCommunication();

  void updateStatus() {
    comm.send(message);
  }

  String get message => _messageElement.value;
}
I still support the Communication implementor, defaulting to the HttpCommunication concrete implementation. I still support the updateStatus() method. I also have code specific to a web page, which grabs the message to post from an input element. But, despite the new web-specific code, the original intent is still the same. I want to be able to make changes to the communication implementation without affecting the WebMessenger abstraction. I still want to be able to assign different Communication objects, in this case at runtime when a different radio button is selected:
  queryAll('[name=implementor]').
    onChange.
    listen((e) {
      var input = e.target;
      if (!input.checked) return;

      if (input.value == 'http')
        message.comm = new HttpCommunication();
      else
        message.comm = new WebSocketCommunication();
    });
So the intent remains the same. And, maybe someday the company will come to its senses and rehire a mobile team. At that point, the abstraction interface can be pulled back out and I will have a true bridge. In the meantime, what do I have?

This sure looks like a simple strategy pattern to me:
class WebMessenger  {
  Communication comm;
  // ...
  void updateStatus() {
    comm.send(message);
  }
  // ...
}
In that sense, a WebMessenger has a Communication object. The specific implementation of Communication can vary depending on what strategy is chosen. So is it a strategy?

I think the answer is that it remains a bridge because of the intent—intent is big in patterns. Even though it winds up looking exactly like a strategy, the structure was originally chosen to support a multiple abstraction structure. Plus, it could easily be refactored to get back to an interface that makes the pattern more explicit.

What I find interesting, however, is what another developer might think should she come along 6 months from now. Without the interface, she should reasonably conclude that the intent is a simple strategy pattern. Under that assumption, she might very well make any number of subsequent design decisions that make it very hard to switch back to a bridge. So in that sense, who cares what the intent was? Unless the intent is made explicit in some form, it would seem that structure trumps intent.

And so, my final answer is that a degenerate abstraction in the bridge pattern does not guarantee that the pattern remains a bridge. More often than not, a degenerate abstraction in the bridge pattern means that it is a bridge pattern no more. In its place is a strategy pattern, intent be damned.


Day #84

No comments:

Post a Comment