Deindustrializing Java With Lambdas

 

Or: Forget your factories!

Recently I saw some folks on HackerNews talking about the Factory pattern in OO programming, and talking about a “FactoryFactoryFactory” in the wild. And as usual, there is someone saying something to the effect of, “Why is everyone so mean to OO patterns? They exist to fulfill a need!” And while this is true, it’s important to remember that a lot of classical OO patterns are translated across multiple languages and exist to solve problems that existed at the time they were drafted. Not all of them are really relevant if you’re working in modern environments.

The Factory Pattern is an excellent example of a pattern that has not aged well, and it’s almost entirely deprecated in Java 8 (and almost every other modern language that supports lambda functions, even partial lambda functions like Python). I’d like to show you some examples of use cases for Factory, and how functional programming (in particular; unnamed functions and function times) make for cleaner code and less work.

Review: Factories & Dependency Injection

The Factory Pattern as originally described is a tool to change call site instantiation of objects from referring to classes (which the new keyword in Java does) to referring to Interfaces, by leveraging the fact that value assignment is “covariant in the type” (meaning that you can specify a type, but provide a more specific value inhabiting that type).

There are a great many examples in literature, but in industry we see Factory patterns most often when abstracting over resources. As an example:

interface KeyValueStore {
   public Value get(Key k);
   public void put(Key k, Value v);
}

interface KeyValueStoreFactory {
   public KeyValueStore provideStore();
}

public class RedisFactory implements KeyValueStoreFactory {
  private RedisConnection conn;
 
  public RedisFactory(String redisHost) {
      conn = new RedisConnection(redisHost);
  }
 
  public provideStore() { return conn; }
}

While somewhat wordy, this lets us start talking about KeyValueStore, confident that our constructor or method arguments will be able to happily construct objects for us:

class Example {
   public void doAnThing(KeyValueStoreFactory factory) {
       KeyValueStore kv = factory.provideStore();
       // ... Do things with the kv, not knowing its specific class.
   }
}

We may also see higher order factories, or “FactoryFactory”, as we go. This is often used as a means of implementing “Dependency Injection”, a pattern by which factory objects are injected into an environment, allowing for drastic changes in the underlying implementation of our dependencies based on configuration.

Dependency Injection is a big topic, so I won’t cover it here, and often uses metaprogramming for automation. But for the sake of example pretend this is valid code:

class KeyValueStoreFactoryFactory {
   private KeyValueStoryFactory f;
  
   public KeyValueStoreFactoryFactory( Config config ) {
       let fconfig.inferFactoryFor(KeyValueStore);
   }
  
   public getFactory() { return f; }
}

This second layer indirection (and in some cases, a third) allows for increased flexibility and more generic code. It also allows for code that’s a lot more “testable”, allowing the factories to transparently offer suitable test mocks without difficulty.

But the cost of this is a very deep, verbose class hierarchy and a confusing ontology, even if it’s formulaic. It often leads to people asking, “Where is the code that actually does things?” And with systems like Guice, it may be that there IS no code local to repository that “does things.”

Review: Lambda Functions

Before we continue, I want to shift gears from Java to TypeScript, which looks very similar to Java. For all Java’s progress with Java 8, there are several limitations with its lambdas that will only increase the complexity of what we’re discussing, and I like to offer somewhat realistic code.

As a reminder, lambda functions are unnamed function values. Any place a value (an int, a string, etc.) could exist, a lambda function is valid. Lambdas also have the property that they “close over” values that they need to compute their value(1).

Example is this classic “let over lambda” pun that creates a counter:

function makeCounter(startV: integer):  {
   let ctr = startV;
   return () => ctr++;
}

function main() {
  let ctr1 = makeCounter(100);
  let ctr2 = makeCounter(0);
 
  for(let i = 0; i < 3; i++) {
      console.log(ctr1());
      console.log(ctr2());
  }
}

/* Prints:
100
0
101
1
102
2 */

Lambda functions exist in Java, but the closed over value (in this case “ctr”) has to be final. In most functional languages with mutable values, we don’t have that restriction.

Simple Factories With Lambdas

The simplest Factory pattern we’ve seen is the “provided value” pattern, where a function expects a value to be generated which it needs. Since functions do this naturally, it’s not a terribly impressive “demo”. But it’s worth noting that things like Java’s Supplier<T> pattern are just one of many simple views over functional patterns.

interface KeyValueStore {
  get(key: string);
  put(key: string, value: any);
}

function demo(provider: () => KeyValueStore) {
   provider().put("easy game", "easy life");
}

let store = new RedisKVStore( process.env.REDIS_URI );
demo( () => store );

This speaks to the simplicity and flexibility of functions, that with no additional ontology about “Factories” we directly encode this. It may seem somewhat pointless to construct a function, since all we’re doing is calling it to provide a constant value. But consider that we’ve actually encoded more flexibility! Note that with just a slight change, we’ve got very different behavior!

demo( () => new RedisKVStore( process.env.REDIS_URI ));

This variant actually creates a fresh KV store on every use, which is great when you want to bog down your Redis server with too many connections, eventually rendering it useless.

Higher Order Factories With Lambdas

Just like with Factory objects, we can imagine a higher order patterns with lambdas. However, unlike Factory objects, the properties of functions make them much more useful in this case. By using the properties of closures we saw earlier, we almost never need to expose the fact that we’re using them to our clients, nor ask our callers to understand that they have a higher order functions.

In particular, languages with mutable values and closures can perform a huge variety of tricks to create the appearance of a simple function call. Let’s consider for a moment what the above demo code; how might we allow configuration between Redis and DynamoDB based on configuration? With Java and a less lambda-oriented mindset, we’d result to complex metaprogramming (e.g., Guice) or expose the idea of a “FactoryFactory.”

Functional composition offers a very different strategy in TypeScript, where we turn a normal function into the kind of singleton factory function we want:


function singleton<A>(producer: () => A): () => A {
  let instance = producer();
  return () => instance;
}

function loadKVStore():  {
   if( process.env.REDIS_URI ) {
       return new RedisKVStore( process.env.REDIS_URI );
   } else if( process.env.DYNAMO_CONNECT_DETAILS ) {
       return new DynamoKVStore( JSON.parse(process.env.DYNAMO_CONNECT_DETAILS));
   }
}

demo( singleton(loadKVStore) );

Take a moment to consider what this code, in particular singleton is doing. It’s a function transformer, turning simple logic into a memoized function. We’ve got a “FactoryFactory” here, but since Functions compose in a way Objects don’t, we’ve managed to shield our caller from that.

What’s also interesting is that we have the opportunity to transform the output of a factory without informing our caller. Factories can do this too, but often expose details to their callers in the process.

As an example of what we might see with a “higher order” lambda factory in this case, let’s consider a unique case; testing to see if a new type of client works on a subset of our users. Consider that we might have two different libraries, and we’d like to test if the new one works on 10% of requests. Rather than writing a unique Factory object, we’d write a generic function:

// A type alias for clarity:
type KVFactory = () => KeyValueStore;
function pSplitFactory( testProbability: float,
                       testCase: KVFactory,
                       normalCase: KVFactory): KVFactory {
   let testClient = testCase();
   let normalClient = normalCase();
  
   return () => {
       if( Math.random() < testProbability ) {
          console.log("Using new library to service KV Requests!")
          return testClient;
       } else {
          return normalClient;
       }
   };
}

demo( pSplitFactory(0.1, loadNewKVStores, loadKVStores) );                       

Post-Industrial Thinking

No matter how intricate our logic gets, our callers never need to think about FactoryFactories or generic parameters or, well, anything. By placing these interfaces behind a functional indirection we offer nearly unlimited flexibility without introducing complex ontologies or advanced metaprogramming techniques into our code. We’ve simply used the properties of functions. While we’ve elected to use TypeScript for brevity in this case, but everything we’ve done here is applicable in Java with Java’s lambda support (except for our counter demo).

Sometimes, it’s astonishing how much mileage something as simple as a lambda function can get, but in this case you really can go a long way. Functions capture a wide range of behaviors easily while obscuring their specifics from downstream consumers. This makes them ideal for replacing complex behaviors or compiler features with simple, value-level constructs.

Next time you find yourself writing a factory, try taking a step back and asking, “Can I just use a darn lambda?”