Wednesday, March 9, 2022

Curly Braces #2: The design phase in Agile Java development

When it comes to the design of agile projects, the goal is to keep everything as simple as possible, while encouraging experimentation.

When you form a startup business, it’s a good strategy to spend money slowly, with the goal of generating revenue quickly. This is also a great one-line summary of the Agile method of software development. If time is your currency, spend it on only the most important tasks, and get something into production early to serve your users. This is well understood. But I still often hear the question, “Where does the software design phase fit into the Agile development process?” This is the topic I’ll explore here, especially as it relates to Java development.

Extreme Programming

With two-week or three-week sprints, it may seem challenging to find time for proper software architecture and design. To answer this challenge, I turn to a precursor of Agile, Extreme Programming (XP), as described in Kent Beck’s book, Extreme Programming Explained. In his book, Beck suggests a design strategy with goals that are aligned with both the process and the overall project goals. As far as I can see, Agile agrees with this strategy.

Just as it’s a myth that there’s little to no planning with the Agile development process, it’s a myth that Agile doesn’t account for design. For example, I’ve been part of more planning and retrospective meetings with projects that follow the Agile process than with any other process. Agile has enabled me to continually think about and consider software design with each sprint. This is quite the opposite of the waterfall software development process, where you consider design only once early in the process.

The notion of “sprint zero,” to me, is another myth. By definition, taking an arbitrary amount of time at the beginning of a project to design or build a backlog isn’t a sprint and runs counter to Agile. It’s effectively water-scrum-fall, a compromise.

There’s a better way to do Agile, and that’s to focus on simplicity.

Some things are inherently complex by their nature, but often complexity has simplicity at its base. The theory for design in Agile is to have just enough design to meet your immediate goals.

I believe in this approach, but I’ll admit there is a risk: If your design is too simple, technical debt can accumulate. There may be sprints where a concerted, collaborative effort is needed to make larger design decisions. However, it’s important to ensure that you’re always doing just the amount of design that’s needed.

Here are some strategies to employ; some are general, but some are Java-specific.

Have a continuous design strategy

It’s best to have a design strategy that carries through to each sprint. The strategy should enforce design concepts that are in line with the project goals and are easily communicated to team members. I’m in favor of small bouts of design with each sprint to continually improve the project structure. However, design is not an ad hoc process. The following elements of a good design strategy should be reinforced with each sprint:

◉ Reduce dependencies. Changes should be localized whenever possible, thus limiting their potential adverse effects.

◉ Consider cross-cutting facilities. Grow the common codebase as more features and components are added over time.

◉ Minimize abstraction. Add abstractions only as needed. (A funny story is recounted in Too Blue! by Dennis Andrews: In the early days of the IBM PC, if IBM identified the need for someone to begin a new role, instead of hiring just one person, the company would start a whole new department in anticipation of growing demand.)

◉ Choose simplicity. To handle both current and future needs, go with simple designs that are easily communicated.

◉ Emphasize collaboration. Communicate intent as you design and code for each sprint. If you cannot easily explain what you plan, consider a simpler approach.

◉ Define the test stories. Stories should include user requirements, new feature descriptions, and detailed directions on how to test the features. After all, if you cannot describe how to test something, how can you be sure you’re properly describing what is to be built? Usually when you describe the test process, you consider how to implement the feature accurately. The result will be a design that’s complete but also concise.

Agile, design, Java, and you

Good software design is key to long-term project success and to maintaining team morale, no matter the language or platform. Still, there are some approaches that apply particularly well to Java projects.

Encourage experimentation. Your Agile process needs to account for experimentation and even encourage it—not on every sprint, perhaps, but certainly experimentation can be helpful throughout the development lifecycle.

For example, the Java ecosystem is rich, and this means you may need to consider the use of one open source or commercial package among many, such as which NoSQL database to use or which storage paradigm to adopt. To make the best decisions, you might need to try the alternatives. This suggests dedicating a sprint, now and then, to conduct experiments.

That said, be realistic. Imagine a possible design decision to build your own NoSQL database for future flexibility. Experimenting with building a NoSQL database engine is likely not the best use of time.

Whereas past development practices stressed designing your own core capabilities to anticipate potential possibilities, Agile doesn’t necessarily support this. Something unanticipated may come along, such as the rise in graph databases, which better suits your application and data. If you built your own NoSQL database, switching means you have wasted all that effort. Or worse, making a big investment writing, debugging, and tuning a NoSQL database engine may delay the move to a graph database that’s perhaps a superior choice, due to an all-too-human desire to justify your prior investment.

Consider design by contract. Java interfaces are used to define contracts, and many developers know this. Contracts serve as a layer of abstraction, because they help break dependencies between discrete classes. In other words, if you write your code to interfaces, you can change the implementing classes without affecting those that use them. This is known as interface decoupling and is directly in line with Agile’s rule of simplicity, with a goal of limiting the side effects of code changes.

For example, say you are implementing an automated barista for a very busy coffee shop. There are different types of beans to use and different brewing methods depending on the order. You can write the Barista class to use different classes directly, each representing the different coffee beans and makers. It is better to abstract the implementations using interfaces: CoffeeBean and CoffeeMaker.

Although the details within the classes are different, each class implements the same set of methods. Therefore, the correct classes can be supplied to the Barista constructor, see the two lines that begin with the keyword new.

public class Barista {

    CoffeeBean coffeeBeans = null;

    CoffeeMaker coffeeMaker = null;

    public Barista(CoffeeBean beans, CoffeeMaker maker) {

        // …

    }

    public boolean brew() {

        coffeeMaker.brew( coffeeBeans );

    }

}

// …

Barista barista = new Barista( 

    new EspressoBeans(), 

    new EspressoMaker() ); 

barista.brew();

The result is a system that’s easy to extend, sprint by sprint, to automate the creation of new coffee drinks.

Avoid cyclic dependencies. Interface decoupling can result in risky dependencies, especially when you’re building software in small, rapid changes.

For example, consider a project that uses four Java packages: A, B, C, and D. Package A has dependencies on packages B and C. Package B has a dependency on package C. Package D has a dependency on Package B (see Figure 1).

In this scenario, what if package C later adds a dependency on package D? You have a nasty cyclic dependency between them.

Oracle Java Development, Oracle Java Exam Prep, Oracle Java Learning, Java Career, Java Skills, Java Jobs
Figure 1. Cyclic dependency in Java packages

This scenario may seem contrived, but it’s more common than you might think in complex applications. It can result in build issues and side effects when making changes to a package and its classes. Cyclic dependencies can easily sneak up on you in an Agile project involving many developers, but proper design effort applied at every sprint will help to avoid it.

Apply the dependency inversion principle (DIP). With DIP, you create the right level of abstraction to avoid highly coupled classes yet allow them to be used together in ways that make sense to break cyclic dependencies.

Specifically, DIP says the following:

◉ High-level components should not import from lower-level components.
◉ Abstractions hide details.
◉ Use interfaces.

In the example in Thorben Janssen’s article on DIP, the familiar topic of coffee is used with the CoffeeBean interface from the earlier code example. However, DIP is important when you consider how CoffeeBean objects are used. They can be ground, brewed, or even eaten as snacks.

Sticking with the traditional notion of making drinkable coffee, you will need a CoffeeMaker object that uses CoffeeBean objects. Since there are many different types of coffeemakers, this application defines an interface also, as shown in Figure 2.

Oracle Java Development, Oracle Java Exam Prep, Oracle Java Learning, Java Career, Java Skills, Java Jobs
Figure 2. DIP, with strict relationship rules, avoids cyclic dependencies.

With DIP, it’s important to keep the relationships proper. Although CoffeeMakers depend on CoffeeBeans, CoffeeBeans should be written with no knowledge of the types of different CoffeeMakers, nor should beans be partial to how they’re roasted, ground, and otherwise prepared.

Implement scalability through tiers. Following simple design strategies so far will help you design for scale later, when you need that scale. With interfaces and DIP, you can easily insert tiers within your codebase as growth requires it without much rework.

For example, early in the implementation of a web application, you may choose JavaServer Pages that write directly to a database. As the project progresses, you might find it’s better to have a thin data tier to abstract your storage choice. As usage grows, you may choose to add yet another tier to offload transaction processing, and so on (see Figure 3).

Oracle Java Development, Oracle Java Exam Prep, Oracle Java Learning, Java Career, Java Skills, Java Jobs
Figure 3. An example of a typical multitiered web application

I find with a distributed architecture, such as components spread across tiers, using a form of messaging middleware helps. It allows you to reduce dependencies and remove tight coupling using anonymous publish-subscribe messaging, asynchronous communication, and reliability. You can use REST, Java Message Service, the Data Access Object pattern, WebSockets, a cloud service, or an edge protocol such as MQTT, for example. This approach helps you design, code, and deploy components independently, increasing your agility. (A useful overview of tiered software design is “Distributed multitiered applications.”)

Source: oracle.com

Related Posts

0 comments:

Post a Comment