Your web browser is out of date. Update your browser for more security, speed and the best experience on this site.

Update your browser
CapTech Home Page

Blog June 14, 2018

Revealing Intent

CapTech

Image of Data StreamingOne of the biggest hurdles that I've come across as a software engineer has been working with code that does a poor job of communicating intent. When I say intent, I mean purpose. You've probably experienced it, too. Some may argue that your skill set dictates how capable you are of understanding code. However, that line of reasoning is a flawed . You're blaming the confusion your code causes on a lack of individual talent rather than your own ability to contribute as a team member. This clouds your ability to structure code for a diverse team of people. Instead, you wind up writing code for your perception of a "capable" engineer. Building quality software takes teamwork and collaboration with people that have varying levels of experience and varying degrees of domain knowledge. If you don't consider this reality as you build and evolve your system, you're doomed to fail because all these people will interact with your code at some point.

This begs the question, how do you code with intent? The simplest way to approach this problem is with a question. Can someone with limited domain knowledge and experience understand your code? This approach requires a bit of empathy and imagination to succeed. Luckily for you, you're human. You should be able to muster a little of both. To illustrate what I'm talking about, I'm going to depict a scenario with a hypothetical set of requirements and give one example showcasing little to no intent and one with intent.

Example

Let's say you work on a video streaming website like YouTube, except it's only offered within the U.S.. Early in the development of this project, you're tasked with developing an API to perform CRUD operations on a User. A User must have an email address, first name, last name, and age. Optionally, if the User is an adult, he or she can have a bio. Some of the videos you serve are age restricted. This means that Users who are minors cannot watch them.

For the purposes of this example, we are primarily going to look at the User class that you implemented for this API, which is written in Java. To add some context, the code base your API is implemented in is structured as a traditional layered architecture. The User class in question was implemented in the domain layer.

Without Intent

Can you discern any of the User requirements up above from the class below?


public class User {

 private String emailAddress;
 private String firstName;
 private String lastName;
 private Integer age;
 private String bio;

 public String getEmailAddress() {
 return emailAddress;
 }

 public void setEmailAddress(String emailAddress) {
 this.emailAddress = emailAddress;
 }
 .
 .
 .

To show you why this class lacks intent, let's analyze it. I'll start off by creating an instance of it.

User user = new User();

You might ask yourself, "What's wrong with that?" Well, the problem is that it tells me that a User can have none of its properties set and still be perfectly valid within the context of this software system. Is that true? If we look back at our requirements, that is wrong. A User must have an email address, first name, last name and age.

So? Can't I just do this?

user.setEmailAddress( "[email protected]" );
user.setAge( 26 );
user.setFirstName( "Andy" );
user.setLastName( "Lulciuc" );

You can, but how are going to ensure that any class that interacts with User is going to respect that implicit contract? The answer is, you can't. The only way another person can understand how to interact with a User is by looking at wherever you decided to implement User instantiation. For now, let's just say you implemented this in UserServiceImpl. This is painful to deal with as a consumer of your code because now I need to look at your implementation. Fundamentally, that is poor interface design because successful integration with your service relies on breaking encapsulation by physically opening the class file and looking at what you did. Imagine if you had to look at the source code of the Facebook API in order to integrate with it effectively. If that were true, no one would use it (or be happy to use it). I shouldn't have to concern myself with how you implemented an interface. It will only slow me down and increase the possibility for misinterpretation. Furthermore, by delegating the domain requirements of User to another class like this, you're implying that there might be another implementation of User instantiation elsewhere. That is confusing.

You might be inclined to say that you can define the requirements of User through an interface.

public interface UserService {

 public static final int MAX_AGE_MINOR = 17;

 /** 
 *
 * @param emailAddress cannot be blank 
 */
 User get( String emailAddress );

 /** 
 *
 * @param emailAddress cannot be blank 
 * @param firstName cannot be blank 
 * @param lastName cannot be blank 
 * @param age cannot be null, cannot be negative, must be greater than max age of a minor
 * @param bio optional 
 */
 void saveAdult( String emailAddress, String firstName, String lastName, Integer age, String bio );

 /** 
 *
 * @param emailAddress cannot be blank 
 * @param firstName cannot be blank 
 * @param lastName cannot be blank 
 * @param age cannot be null, cannot be negative, must be less than or equal to max age of a minor
 */
 void saveMinor( String emailAddress, String firstName, String lastName, Integer age );
}

This is certainly better than nothing, but the reality is that you still can't be sure what it means to be a User in your system without looking at the implementation of the UserService.

public class UserServiceImpl implements UserService {

 @Override
 public void saveAdult(
 String emailAddress,
 String firstName, 
 String lastName,
 Integer age,
 String bio ) {

 Validate.notBlank( emailAddress, "'%s' cannot be blank", "emailAddress" );
 Validate.notBlank( firstName, "'%s' cannot be blank", "firstName" ); 
 Validate.notBlank( lastName, "'%s' cannot be blank", "lastName" ); 
 Validate.notNull( age, "'%s' cannot be null", "age" );
 Validate.isTrue( age > 0, "'%s' cannot be negative", "age" ); 
 Validate.isTrue( age > MAX_AGE_MINOR, "'%s' must be greater than max age of a minor", "age" );

 User user = new User();
 user.setEmailAddress( emailAddress ); 
 user.setAge( age ); 
 user.setFirstName( firstName ); 
 user.setLastName( lastName );
 user.setBio( bio );
 .
 .
 .
Is there a better way?

With Intent

How can you implement the User class so that the intent of it is clear?

public class User {

 public static final int MAX_AGE_MINOR = 17;

 public enum Type {
 ADULT,
 MINOR
 }

 private final String emailAddress;
 private final Type type;
 private final String firstName;
 private final String lastName;
 private final Integer age;
 private final String bio;

 /**
 *
 * @param emailAddress cannot be blank
 * @param type cannot be null
 * @param firstName cannot be blank
 * @param lastName cannot be blank
 * @param age cannot be null, cannot be negative
 * @param bio optional
 */
 private User(
 String emailAddress,
 Type type,
 String firstName,
 String lastName,
 Integer age,
 String bio ) {

 Validate.notNull( emailAddress, "'%s' cannot be blank", "emailAddress" );
 Validate.notNull( type, "'%s' cannot be null", "type" );
 Validate.notBlank( firstName, "'%s' cannot be blank", "firstName" );
 Validate.notBlank( lastName, "'%s' cannot be blank", "lastName" );
 Validate.notNull( age, "'%s' cannot be null", "age" );
 Validate.isTrue( age > 0, "'%s' cannot be negative", "age" );

 this.emailAddress = emailAddress;
 this.type = type;
 this.firstName = firstName;
 this.lastName = lastName;
 this.age = age;
 this.bio = bio;
 }

 /**
 *
 * @param emailAddress cannot be blank
 * @param firstName cannot be blank
 * @param lastName cannot be blank
 * @param age cannot be null, cannot be negative, must be greater than max age of a minor
 * @param bio optional
 */
 public static User adult(
 String emailAddress,
 String firstName,
 String lastName,
 Integer age,
 String bio ) {

 Validate.notNull( age, "'%s' cannot be null", "age" );
 Validate.isTrue( age > 0, "'%s' cannot be negative", "age" );
 Validate.isTrue( age > MAX_AGE_MINOR, "'%s' must be greater than max age of a minor", "age" );

 return new User(
 emailAddress,
 Type.ADULT,
 firstName,
 lastName,
 age,
 bio );
 }

 /**
 *
 * @param emailAddress cannot be blank
 * @param firstName cannot be blank
 * @param lastName cannot be blank
 * @param age cannot be null, cannot be negative, must be less than or equal to max age of a minor
 */
 public static User minor(
 String emailAddress,
 String firstName,
 String lastName,
 Integer age ) {

 Validate.notNull( age, "'%s' cannot be null", "age" );
 Validate.isTrue( age > 0, "'%s' cannot be negative", "age" );
 Validate.isTrue( age <= MAX_AGE_MINOR, "'%s' must be less than or equal to max age of a minor", "age" );

 return new User(
 emailAddress,
 Type.MINOR,
 firstName,
 lastName,
 age,
 null );
 }
 .
 .
 .
What does the UserService look like?
public interface UserService {

 /** 
 *
 * @param emailAddress cannot be blank 
 */
 User get( String emailAddress );

 /** 
 *
 * @param user cannot be null 
 */
 void save( User user );
}

Isn't that easy to comprehend? The User class defines what is means to be a User. The UserService defines how to retrieve and store Users. The purpose of each class is clear and the requirements that define them within this domain are clearly represented.

Conclusion

To be clear, coding with intent is about more than just applying a specific pattern, it's about communicating the purpose of your code. Whether it's in your domain layer or presentation layer, you took the time to analyze the implications of what you wrote, and you thought about the consequences of someone missing your intent. In the case of the User class without intent, if someone neglected to look at how a User was being created, there might be negative consequences.