Test-Driven Decoupling

Agile India 2013

Owen Rogers / @exortech

https://github.com/exortech/presentations

Hi!

Owen Rogers / @exortech

Product Lead @ Pulse Energy

Practicing Agile/XP since 2000

Formerly an agile coach with ThoughtWorks

Conference organizer for original Agile India conference

Anyone know what this is?

This one's for the sci-fi geeks

Dealing with monolithic systems

.WAR

.DLL

.GEM

mon·o·lith·ic

characterized by massiveness, total uniformity, rigidity, invulnerability, etc.

Where does the Monolith come from?

Evolving systems

Canalizing Design

http://michaelfeathers.typepad.com/michael_feathers_blog/2009/06/canalizing-design.html

Make HAL do something else

							
function askHAL(action) {
	if (isReadMe(action) {
		return "Affirmative, Dave. I read you.";
	} else if (isReadMessage(action)) {
		return "It is dangerous to remain here. You must leave within two days.";
	}
	return "I'm sorry, Dave. I'm afraid I can't do that.";
}
							
						

Path of least resistance

ac·crete

  1. Grow by accumulation or coalescence
  2. Form (a composite whole or a collection of things) by gradual accumulation

Big Ball Of Mud

Problems of Monolithic Systems

  • Code encumbrance
  • Regression cost
  • Scaling
  • Resilience

What's the alternative?

Service-oriented architecture

SOA Nirvana - WHEE!

Meanwhile, back in the real world...

Barriers to SOA

Design barriers

  • Domain model is evolving
  • Code base is rapidly evolving
  • Service context boundaries
  • Conway's law
  • Overhead of splitting and merging services

Technology barriers

  • Message protocol (JSON? BJSON? BSON? Protobuf? Thrift? SOAP?)
  • Message design
  • Serialization/deserialization overhead
  • Message versioning
  • Language/library support

Deployment barriers

  • Dependency management
  • Service versioning
  • Interdependent builds
  • Multiple packages to deploy
  • Monitoring and support

So, how do we get from

Or Even

+

We want to split the monolith

But how?

Easy right?

  1. Pull related functionality into a separate module
  2. Package module as a service
  3. Deploy
  4. Profit!

Not so fast, Dave

Alien goo

First, a brief digression...

Who hasn't read this book?

Package design

Package = directory, namespace, module, etc.

Maximize cohesion within a package

Minimize coupling

Look familiar?

What's wrong with this model?

Let's look inside

Ewww - maximum alien goo

Structure packages by domain context

Makes extracting into services much easier!

So back to

+

Modularization Process

  1. Pull related functionality into a common package (maximize cohesion)
  2. Restructure code to sever unnecessary external dependencies (minimize coupling)
  3. Extract package into an independent component/module
  4. Servicify module
  5. Profit!

Great! But this is hard...

Test-driven decoupling

(to the rescue!)

Test-driven decoupling

Use tests to enforce package structure and support decoupling effort

Tools

Free, open source tools that you are probably already using

JDepend

http://clarkware.com/software/JDepend.html

NDepend

http://www.ndepend.com/

MaDGe - Module Dependency Graph

https://github.com/pahen/node-madge/

What about external dependencies?

I'm sorry, Dave. This mission is too important for me to allow you to jeopardize it.

Focus on the code you own

Examples

+

Example 1: Starting point

core → util

						
  @Test
  public void corePackageShouldOnlyDependOn_util() throws Exception {
    Set<String> valid = ImmutableSortedSet.of("com.pulseenergy.util");
    assertThat(findInternalDependencies("com.pulseenergy.core"), is(valid));
  }
						
					

system → { util, core }

						
  @Test
  public void systemPackageShouldOnlyDependOn_core_util() throws Exception {
    Set<String> valid = ImmutableSortedSet.of(
      "com.pulseenergy.core", "com.pulseenergy.util");
    assertThat(findInternalDependencies("com.pulseenergy.system"), is(valid));
  }
						
					

Example 2: Invalid dependency

security → { core, spring, system, util }

						
  @Test
  public void securityPackageShouldOnlyDependOn_core_springSecurity_system_util() {
    Set<String> valid = ImmutableSortedSet.of(
      "com.pulseenergy.core", "com.pulseenergy.spring.security",
      "com.pulseenergy.system", "com.pulseenergy.util");
    Set<String> invalid = ImmutableSortedSet.of("com.pulseenergy.web.filter"); // invalid dep
    assertThat(findInternalDependencies("com.pulseenergy.security"),
      is(ImmutableSortedSet.of(valid, invalid)));
  }
						
					

Test Writing Process

  1. Write a failing test to capture the existing dependencies (Red bar)
  2. Assert all existing dependencies (Green bar)
  3. Split dependencies into valid and invalid sets
  4. Remove one invalid dependencies from the set (Red bar)
  5. Break that dependencies in the code (Green bar)
  6. Lather (commit) and repeat

1. Capture the existing dependencies: foo → ???

							
  @Test
  public void fooShouldDependOnX() {
    assertThat(findInternalDependencies("com.pulseenergy.foo"),
    	is(Collections.emptySet()));
  }
							
						

2. assert existing dependencies (green bar)

							
  @Test
  public void fooShouldDependOnX() {
    assertThat(findInternalDependencies("com.pulseenergy.foo"),
    	is(ImmutableSortedSet.of("com.pulseenergy.bar", "com.pulseenergy.baz")));
  }
							
						

3. split into valid/invalid sets (green bar)

							
  @Test
  public void fooShouldDependOnBar() {
    Set<String> valid = ImmutableSortedSet.of("com.pulseenergy.bar");
    Set<String> invalid = ImmutableSortedSet.of("com.pulseenergy.baz");
    assertThat(findInternalDependencies("com.pulseenergy.foo"),
    	is(Iterables.concat(valid, invalid)));
  }
							
						

4. remove invalid dependency from test (red bar)

							
  @Test
  public void fooShouldDependOnBar() {
    Set<String> valid = ImmutableSortedSet.of("com.pulseenergy.bar");
    assertThat(findInternalDependencies("com.pulseenergy.foo"), is(valid));
  }
							
						

5. remove invalid dependency from code (green bar)

Breaking dependencies can take a long time

Commit tests with invalid dependencies listed

Run these tests in your build

You broke the build, Dave █

When package is suitably decoupled, service-ify!

Thanks!

owen@exortech.com | @exortech

For serious geeks only

Under the hood


private Collection<JavaPackage> analyzePackage(String targetPackage) throws IOException {
	final JDepend jDepend = new JDepend();
	jDepend.addDirectory(IntrospectionHelper.findClassesDirectory().getAbsolutePath()
		 + "/" + convertPackageToPath(targetPackage));
	@SuppressWarnings("unchecked")
	final Collection<JavaPackage> allPackages = jDepend.analyze();
	final List<JavaPackage> filteredPackages = Lists.newArrayList();
	for (JavaPackage aPackage : allPackages) {
		if (aPackage.getName().startsWith(targetPackage)) {
			filteredPackages.add(aPackage);
		}
	}
	return filteredPackages;
}
						

Under the hood


private Set<String> findInvalidDependencies(JavaPackage javaPackage) {
	final Set<String> packageViolations = Sets.newTreeSet();
	@SuppressWarnings("unchecked")
	Collection<JavaPackage> efferents = javaPackage.getEfferents();
	for (JavaPackage efferentPackage : efferents) {
		if (isInternalDependency(efferentPackage)) {
			packageViolations.add(efferentPackage.getName());
		}
	}
	return packageViolations;
}