scala cake pattern explained - Part 1 (and we will use a monad!)

I realized I need to be able to explain the differences between the cake pattern in scala and spring dependency injection. As in my last post, I wanted to understand how to compose objects to build applications. I've become a scala fan because alot of things make sense, although it takes a long time (at least for me) to learn a more functional style.

The reason I want to compare the cake pattern to spring dependency injection is because each approach has trade-offs. I'll try to call those out more carefully than I have seen written elsewhere. There are a number of blogs you can find on the cake pattern and it is a pattern often covered in functional programming books.

Here we go...

Composing Using Plain Old Java

Lets ignore all dependency injection for the moment and just think about plain java. We want to compose objects. For example, we have a software component that is dependent on other components. In java, we have two ways to handle this.

Inheritance and Bean Properties

Using inheritance, your target component can obtain a dependency from subclassing. An abstract superclass can hold a variable of the dependent component.  The pattern looks like:

public class DependentComponentSuperclass {
  protected ServiceComponent dependency;
  
  public void setDependency(ServiceComponent c) { ... }
  public ServiceComponent getDependency() { ... }
...
}

public class TargetComponent extends DependentComponentSuperclass {

   public void init() { 
      // Either set a default dependency or have it set
      // in another part of the program
      // ...
   }

...
}

You also have the option of using an interface to specify that a bean property should be set, although its up to the runtime execution to set the bean property with the dependency.

public interface ServiceComponentAware {
  void setDependency(ServiceComponent c);
  ServiceComponent getDependency();
}

public class TargetComponent extends ServiceComponentAware {

  public void setDependency(ServiceComponent c) { ... }
  public ServiceComponent getDependency() { ... }
...
}

The problems with the above model are well known. For small systems, this is not a big deal and this model works fine. As the system grows, 5-10 services and many variations of components, the inheritance and bean property approach makes the overall application inflexible to change as well as maintenance. But its a valid model to use. You need to note that you can create the component at compile-time by usign the init() method to set the dependency variable, or at runtime, where the bean setter is used.

Its also important to note that the intent of the ServiceComponentAware interface is really to inform the programmer that the concrete class expects to have the ability to store and retrieve a ServiceComponent object. Its really communicating a composition concept using an interface and methods.

Dependency Injection in Java

In java, if I want to do dependency injection, you typically need to use a DI framework such as guice or spring. Using these frameworks allows you to declare your objects (in code or XML), the wiring approach (this object needs this other object to be fully initialized) and any wiring constraints (required or not required). I use spring mostly, so the I am using spring in this blog but the ideas also apply to guice.

The model above becomes

public class TargetComponent {
   @Autowired(required=true)
   ServiceComponent dependency;

   @PostConstruct

   public void init() { 
      if(dependency != null)
         dependency = new ServiceComponentImpl();
         // Or thrown an exception if its not set
   }
...
}

Notice that a few things are going on. 
  • You need to use the framework to arrange for the ServiceComponent to be created and inject. You can use XML bean specifications, java code configuration (@Configuration), etc.
  • The dependency constraint could be that the component is required or not-required.
  • You can inject by name or type (the above injects by type).
  • The injection occurs at runtime not compile time--the DI framework does all the work. I do indicate that like the pure java approach, you can set the dependency variable in an init() method and ensure that the init() method is called using @PostConstruct.
With spring, you have a few ways to control the dependency and in my opinion, its easy to identify that the ServiceComponent is to be injected because of the annotation @Autowired.

Using the DI container gives you the flexibility to arrange for the dependency variable to be satisfied at run-time. 

Deeper Thoughts

The DI framework options works well, but you have other options with scala. First lets note a few salient points about what we are trying to achieve.
  • Bean properties that set service component objects (dependencies for the target object) are really informing the programmer that it expects a dependency object to be set. This is communicating intent through interfaces and methods, versus say, a mechanism to force an object to have a variable that holds the dependent service component.
  • We really want to avoid type hierarchies that require the programmer to inherit from because this creates greater coupling the code and reduces flexibility for change. For larger programs, this will make the program difficult to create.
  • It would be nice to have an option that uses the compiler to enforce the composition versus the DI framework. Setting the variable by using the compiler does not remove the fact that the variable is still set during the runtime of the program, but it the compiler can make sure that it is enforced for us with no other framework being required other than what the compiler uses and requires as a runtime.
    • Note that when we say set a dependency at compile time, we are really saying we want the compiler to ensure, to the best of its ability, that the dependency variable is set based on what the compiler promises it will do for us versus relying on a programmer to ensure a DI framework is configure correctly. Again, we are shifting responsibility for the desired outcome from one piece of infrastructure to another--a DI framework to the compiler framework
In scala, since its fairly compatible with java, you can use any of the java or DI framework approaches.

Cake Pattern

The cake pattern is an approach based on the compiler infrastructure to ensure that we can compose programs and be reasonably confident that it is configured correctly. It is possible to use the cake pattern and still not have a correctly configured program, but when the cake pattern is combined with other aspects of scala's runtime, it can provide strong assurance that it is reasonably configured. In this sense, "compose" means that the programmer can ensure that the types are seen in the target objects and therefore available for use.

I speak of configuration in the sense of having valid values for the dependencies. A compiler based approach can make sure that the dependent types are known, but there must still be a way to constrain the program to ensure it has valid objects. This is much like using the @Autowired(required=true) annotation in spring, even though you may compose your java objects to have the right types as dependency in your variables in that target class, you still have to make sure its not set to null or some other invalid value.

So we need to ensure that the composition is possible and enforced through the compiler and that is configured correctly. You need both to have a valid program. Spring DI frameworks helps you with both, although its not foolproof. The cake pattern in scala helps you with both although its not perfect.

The cake pattern also expresses the intent of setting dependencies but does so using valid scala code versus annotations. There is nothing wrong with annotations of course, but annotations are usually runtime type constructs at least when DI frameworks are involved.

Cake Pattern Example

Lets use an expanded example and employ the cake pattern using scala. We are going to cast the composition using the cake pattern then observe that there are different ways to compose the cake pattern in scala and see that there are some trade-offs that have less desirable options but are workable.

In scala, the cake pattern is formed when you use a trait and require any trait using the dependent trait to declare a variable and potentially some supporting types. You can do this in scala. In java, a superclass, the closest thing to a trait in scala that has default implementations, can only enforce that a consuming class have methods defined in that superclass. In java, these methods are sometimes bean properties and as we have seen, what the java bean property methods are really communicating that the concrete class should have a dependent variable set on it e.g. that it should effectively declare a dependent variable.

So with scala, we can use the language to enforce the idea that consumers of the trait has to declare vals (vars or object definitions) as well as supporting types.  Many of the cake examples use traits such as WidgetServiceComponent and declare a val and a sub-trait, such as WidgetService. However, if declaring a sub-trait is too much work, you can just enforce that a function type be defined e.g. (Unit)=>Int.

Its important to realize that if you are using the compile for dependency injection, someone has to type code in that creates the objects. That's what you, as the programmer, are doing when you setup your vals inside your "container" object. It may seem like you could just do that anywhere, and you would be right, but the cake pattern ensures that you have to create the right set of vals. When you think about it, the traits that are used as layers in the cake pattern, that when used, enforce the creation of those vals, is just like writing an XML file for spring containers. The XML file is a skeleton of what objects you want created, the spring container code creates those objects. In the cake pattern, the traits that enforce val creation are like the XML file, and the trait / class / object where the vals are defined (at the last possible point in the program) is like the spring application context code that reads the XML and creates the beans. Of course it feels very manual, because you are using just plain old scala and the compiler to implement the pattern!

The domain for our cake example will be flatfile parsing. There are many variations of flatfile formats and if we want to be able to have deep error information when a parsing error occurs, we need to have a fairly robust infrastructure. So the system below looks a bit much for flatfile parsing (say a CSV) file but is flexible enough to handle ragged right columns, different line formats on different rows and split lines. Note that the below has the cake pattern, existential types, type aliases and a variety of other techniques also included.

The code example is extensively annotated so instead of reading more text, read the code and the annotations. It will explain a lot and describe how the pattern is implemented. I'll then work through several questions you should ask yourself about this code and make some modifications to address issue that we should be aware of when using the cake pattern.

The code is presented in subsequent blogs. First, we will write an initial, crude version, refine it, then use the cake pattern as the last step.

Comments

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO