Evolutionary architecture and emergent design: Environmental considerations for design, Part 2

The impact of refactoring and enterprise architecture

Enterprise software projects don't exist in a vacuum. Environmental considerations have an impact even on decisions that seem purely technical. This Evolutionary architecture and emergent design installment continues to investigate some of those environmental concerns, in particular refactoring and the intersection between architecture and design.

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.

Neal FordNeal Ford is a software architect and Meme Wrangler at ThoughtWorks, a global IT consultancy. He also designs and develops applications, instructional materials, magazine articles, courseware, and video/DVD presentations, and he is the author or editor of books spanning a variety of technologies, including the most recent The Productive Programmer. He focuses on designing and building large-scale enterprise applications. He is also an internationally acclaimed speaker at developer conferences worldwide. Check out his Web site.



30 November 2010

Also available in Chinese

About this series

This series aims to provide a fresh perspective on the often-discussed but elusive concepts of software architecture and design. Through concrete examples, Neal Ford gives you a solid grounding in the agile practices of evolutionary architecture and emergent design. By deferring important architectural and design decisions until the last responsible moment, you can prevent unnecessary complexity from undermining your software projects.

In the last installment, I began discussing environmental considerations for software design. Enterprise software development never happens in a vacuum; clear-cut technical decisions become muddied by politics and other external factors. This installment continues that train of thought, covering issues around refactoring and isolating architectural changes.

Intelligent refactoring

Refactoring, defined by Martin Fowler's seminal book on the subject (see Resources), is a common, well-understood technique for improving code. One of the key enablers for emergent design is the ability to harvest and use the idiomatic patterns you uncover. In the mechanical sense, that means refactoring. But refactoring also encompasses environmental factors for your project. All major IDEs now support refactoring — but you can't rely on a tool to perform intelligent refactorings, merely correct ones.

In the fourth installment of this series, I discussed the Single Level of Abstraction Principle (SLAP), using a method from a sample e-commerce site as the target of refactorings to improve its clarity. That method appears in Listing 1:

Listing 1. An addOrder() method from a sample e-commerce site
public void addOrder(ShoppingCart cart, String userName,
                     Order order) throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    Statement s = null;
    ResultSet rs = null;
    boolean transactionState = false;
    try {
        s = c.createStatement();
        transactionState = c.getAutoCommit();
        int userKey = getUserKey(userName, c, ps, rs);
        c.setAutoCommit(false);
        addSingleOrder(order, c, ps, userKey);
        int orderKey = getOrderKey(s, rs);
        addLineItems(cart, c, orderKey);
        c.commit();
        order.setOrderKeyFrom(orderKey);
    } catch (SQLException sqlx) {
        s = c.createStatement();
        c.rollback();
        throw sqlx;
    } finally {
        try {
            c.setAutoCommit(transactionState);
            dbPool.release(c);
            if (s != null)
                s.close();
            if (ps != null)
                ps.close();
            if (rs != null)
                rs.close();
        } catch (SQLException ignored) {
        }
    }
}

In the SLAP installment, I demonstrated how much more difficult it is to read code that jumps from one abstraction level to another, and I refactored the code in Listing 1 to improve its readability. However, in that installment, I showed the end result of the refactoring, not the intermediate version, which appears in Listing 2:

Listing 2. Intermediate refactoring stage of the addOrder() method
public void addOrder(ShoppingCart cart, String userName,
                     Order order) throws SQLException {
    Connection connection = null;
    PreparedStatement ps = null;
    Statement statement = null;
    ResultSet rs = null;
    boolean transactionState = false;
    try {
        connection = dbPool.getConnection();
        statement = connection.createStatement();
        transactionState =
                setupTransactionStateFor(connection,
                        transactionState);
        addSingleOrder(order, connection,
                ps, userKeyFor(userName, connection));
        order.setOrderKeyFrom(generateOrderKey(statement, rs));
        addLineItems(cart, connection, order.getOrderKey());
        completeTransaction(connection);
    } catch (SQLException sqlx) {
        rollbackTransactionFor(connection);
        throw sqlx;
    } finally {
        cleanUpDatabaseResources(connection,
                transactionState, statement, ps, rs);
    }
}

The code in Listing 2 shows the inherent weakness of any automated refactoring tool. The tool must adhere to a strict contract: the resulting code must still work just as it did before. When you perform an extract method refactoring, for example, the tool must make sure that all variables the extracted method needs are still available — but it can only ensure their presence via parameter passing. Another way of solving the same problem moves the shared variables up to the class level, making them fields of the class. The refactoring tool won't do this because it can't take account of the serious implications — such as threading concerns, naming, and availability in other methods — that such a decision entails. A developer, though, can decide to do a round of manual refactoring that considers any such issues. Manual refactoring of addOrder() results in the code shown in Listing 3:

Listing 3. Manually refactored, greatly improved addOrder() code
public void addOrderFrom(ShoppingCart cart, String userName,
                     Order order) throws Exception {
    setupDataInfrastructure();
    try {
        add(order, userKeyBasedOn(userName));
        addLineItemsFrom(cart, order.getOrderKey());
        completeTransaction();
    } catch (Exception condition) {
        rollbackTransaction();
        throw condition;
    } finally {
        cleanUp();
    }
}

This is the code I ultimately refactor into its own idiomatic pattern in "Leveraging reusable code, Part 1." It's much better than the original code because you can see what it's doing, allowing harvesting of the reusable parts.

You cannot blindly expect a tool to make good decisions for you. It can ensure correct code, but not the best code. To find reusable assets effectively, you frequently must go beyond what automated tools can provide. You also must harvest the collective intelligence of your other team members.

Collective code ownership

Broken windows

In The Pragmatic Programmer (see Resources), Dave Thomas and Andy Hunt borrow the concept of broken windows from studies about abandoned buildings. Deserted buildings generally aren't damaged until a window is broken. That first broken window indicates that no one cares about the property, and the general disrepair and abuse of the building accelerate thereafter.

Broken windows occur in software development too. When you see some code that's not technically a bug but isn't quite right from a design standpoint, you've found a broken window. Collective code ownership says that you must fix that code. Part of the reason that software projects tend to become more fragile and brittle over time is the presence of hundreds (or thousands) of broken windows. If you fix them routinely, your code can get stronger with age, not weaker.

My projects always use pair programming, and we're always on the lookout for broken windows. But we don't automatically drop what we're doing to attack those problems as soon as we find them. When my pair and I discover an error, we assess how long it will take to fix it. If it will consume less than 15 minutes, we'll go ahead and fix it inline with whatever other story we're working on. If the change is more involved, we add it to a technical-debt backlog. All my projects have a technical-debt backlog, maintained by the technical lead. When we get slack time on the project, the tech lead assigns stories from this backlog to eat away at accrued technical debt gradually.

I've been making the argument throughout this series that in software you can't divorce design from coding. The complete source code is the only truly accurate design artifact, which suggests that working on a software project with a group of people is a collaborative design exercise. Thinking about software creation as collaborative design makes some puzzling facts about software development suddenly make more sense. For example, the industry has known for a long time that communication is critical on software projects. (In fact, many agile methodologies consider this a critical requirement for success.) If writing software were like manufacturing, you would need far less communication overhead. Collaborative design requires communication among members.

Collaborative design also suggests that developers are responsible for the correctness and quality of the parts of the overall application they create. Correctness has two facets: adherence to the business requirements and technical suitability. Business-requirements correctness is determined by whatever verification mechanism your company ecosystem has in place to judge the software's suitability to the problem it was written to solve. Technical correctness is left to the development team.

Different teams put their own procedures and mechanisms in place to ensure technical quality, such as code reviews and automated metrics tools run via continuous integration. One practice that many agile teams employ that's a key enabler of emergent design is collective code ownership, which suggests that every person on the project has ownership responsibilities for all the code, not just the code he or she has written.

Collective code ownership requires a level of awareness of the ongoing quality of your project's code, especially with the aim of chasing and fixing obsolescent abstractions and broken windows. More specifically, it requires:

  • Frequent code reviews (or real-time code reviews such as pair programming) to make sure everyone is leveraging common idiomatic patterns and other useful design discoveries by the collective group.
  • Awareness by everyone on the project of at least some of the details of all parts of the project.
  • Willingness for anyone on the project to jump in and fix broken windows regardless of the original author(s) of the code.

Refactoring concerns, both mechanical and environmental, affect emergent design decisions. Another environmental concern for emergent-design efficacy revolves around isolating architectural changes.


Isolating architectural changes

In this series, I've separated design issues from architectural issues but, of course, that separation is harder in the real world. Your architecture is the foundation for all your design decisions, and architectural considerations can affect your ability to use some of the emergent design techniques I've discussed.

Rampant genericness

One common architectural disease in software is rampant genericness, embodied in the idea that if you add lots of layers for extension, you can easily build more on to it later. It is true that having extension mechanisms in place early makes it easier to extend later. But you add complexity as soon as you add those layers, not when you start using them. That extra overhead is a form of accidental complexity until it starts showing actual benefits in your project.

A portion of emergent design is the ability to see and harvest idiomatic patterns from existing code. This skill is hard enough to develop if you have all the code in one physical tier, and it gets even harder when you start taking artifacts that are logically one thing but become split across multiple layers because of an architectural decision. For example, let's say you have a Country class that has a validation rule for the length of the country name. Ideally, you'd like to take advantage of locality and place the validation code as close to the domain class as possible, perhaps in an annotation (an example of exactly this scenario appears in "Leveraging reusable code, Part 2"), as shown in Listing 4:

Listing 4. A MaxLength attribute for a Country class
public class Country {
	private List<Region> regions = new ArrayList<Region>();

	private String name;
	
	public Country(String name){
		this.name = name;
	}
	
        @MaxLength(length = 10)
	public void setName(String name){
		this.name = name;
	}
	
	public void addRegion(Region region){
		regions.add(region);
	}
	
	public List<Region> getRegions(){
		return regions;
	}
}

As long as your code all exists in one logical tier, the code in Listing 4 works nicely. But what happens when the architecture insists that the code that performs the validation must come from a services tier? Now you are left with a no-win decision: do you make the attribute understand your application's architectural tiers and call through to the service layer to do the validation? Or do you move all the validation code into the services tier, removing it from the domain class that should be the owner of that knowledge?

Layers make design decisions harder. I'm not suggesting that you do away with layered architectures (even ones that cut across logical domain tiers). Rather, I'm suggesting that the benefits that layered architectures provide come at a cost, and you should assess the cost before adding an architectural element such as a service layer. Which brings up another issue: when should you add an architectural element that may have an impact on your design?

Architecture vs. design

Throughout this series, I've been relying on the simplistic yet accurate definition of architecture in software: "Architecture is the stuff that's hard to change later." When you look at parts of your project, you can identify architectural elements by asking, "Will it be hard to change this later?" Layers (either physical or logical) are architectural under this definition because it's hard to change (or remove) them later.

Some layers make development (and design) easier. Separating technical responsibilities using a pattern like Model-View-Controller from the Gang of Four book (see Resources) makes it easier to isolate routing, view, and domain logic. However, some architectures further separate the domain tier, creating layers to allow centralization of code. For example, service-oriented architectures create different types of layers to change the topology of your application. Too much architectural layering (especially before it is needed) makes design harder because you shift the foundation the design must rest upon. If it's hard to see a harvestable idiomatic pattern in code running within a single virtual machine, think how much harder it is both to identify functionality and to figure out how to harvest it for reuse across layers.

At least a portion of this problem arises because we incentivize architects incorrectly. One important incentive for architects is to make sure that the entire enterprise ecosystem continues to function, even in the face of changing requirements and capabilities. In software, we suffer from what former U.S. Secretary of Defense Donald Rumsfeld called "unknown unknowns." Software is rife with unknown unknowns — situations you think you understand at the outset only to discover that the problem is different from your first impressions (and typically much harder). How do you deal with unknown unknowns in architecture? You try to overengineer everything. I'm convinced that many projects in the early 2000s included Enterprise Java™Beans (EJB) technology because the question "Are we ever going to need declarative, distributed transactions?" was answered with, "We don't know." The safe bet then was to include EJBs in the project in case it needed them later. However, that made the project more complex from the outset. Ironically, many of the mechanisms put in place for extensibility put the initial release of a project in jeopardy because the extra complexity forces the project to go over time and budget.

Here's an example from a project I was on. One of the hard requirements for this project was internationalization — in version two of the project. The project started with code to handle internationalization because it was viewed initially as an architectural decision: it's so pervasive, it'll be hard to change later. This feature manifested itself as accidental complexity: even simple stories became complex because we had to accommodate this requirement. It was taking so much time that it started placing the ship date for version one in jeopardy. The tech lead made the decision to rip all the internationalization code out of the project, realizing that we wouldn't need that feature in version two if we never shipped version one! In the end, when it came time to work on version two, one of the developers had come up with a clever way, using metaprogramming, to weave the internationalization code into the code base. In other words, he managed to convert an architectural element into a design element, making it something that wasn't so hard to change later. Preemptively making something an architectural element (something hard to change later) sometimes blinds you to what it would look like as a lighter-weight design element.

An uneasy tension exists between architecture and design. Ideally, you want to defer design decisions to the last responsible moment. However, architectural decisions affect the kinds of design decisions you can make. Remember the second part of the definition of architecture: "There should be as little of that stuff as possible." Try to put off architectural decisions until the last responsible moment as well, even though they have a bigger potential impact. You'll be surprised how something that seemed so difficult to retrofit may not be so bad after all.


Conclusion

In this installment and the preceding one, I've discussed environmental concerns and impacts on emergent design. Refactoring is obviously an important tool for emergent design — one that entails both mechanical and environmental concerns. Rather than merely rely on automated refactoring tools, make sure that you do intelligent refactorings. And set up your project environment to enable collective code ownership, leveraging the best of all the people on the project.

Architecture, too, can have an impact on your design options. As with design elements, try to defer architectural decisions to the last responsible moment. Architectural elements put in place to make it easy to extend in the future manifest as accidental complexity until you start using those elements.

The next Evolutionary architecture and emergent design installment will wrap up the series. In it, I'll summarize concepts and draw conclusions from almost two years of writing and presenting about emergent design.

Resources

Learn

Discuss

  • Get involved in the My developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=587987
ArticleTitle=Evolutionary architecture and emergent design: Environmental considerations for design, Part 2
publish-date=11302010