Edit Page

Lab 4: The builder and factory method design patterns

The goal of this lab is to apply two creational design patterns: the builder and factory method design patterns.

Design patterns are proven solutions to solve recurring design problems to design flexible and reusable object-oriented software; that is, objects that are easier to implement, change, test and reuse. The builder design pattern and factory method design pattern are two of the twenty-three well-known Gang of Four (GoF) design patterns. They are classified as creational patterns since both patterns provide an elegant and an efficient way to create an object.

The builder pattern is used to build and construct a complex object in a step by step approach. It’s especially helpful when the creation of an object involves a set of required and optional parameters. Instead of using constructor overloading or telescoping constructor (i.e., the use of more than one constructor in an instance class), the builder design pattern solves this problem in an elegant way allowing us to create different representations of an object. The builder design pattern solves the problem of creating complete objects step by step without having to rely on constructor overloading and passing nulls for unused parameters.

The factory method design pattern is another creational design pattern that deals with the problem of creating objects without having to specify the exact class of the object that will be created. This done by a separate operation (factory method) for creating an object. Creating an object by calling a factory method helps avoid tight coupling between the creator and the concrete classes.

Video

Objectives

In this lab you will

  1. understand a real-world scenario and choose when to apply the appropriate design pattern.
  2. design and implement the builder design pattern.
  3. design and implement the factory method design pattern.
  4. write unit tests and apply Test-Driven Development (TDD).

Requirements and Tools

Getting Started

If your instructor is using GitHub classroom, you will need to accept the assignment using the link below, clone the template repository, and import it as a project into your IDE.

If your instructor is not using GitHub classroom, clone and import the template project at https://github.com/cpit252/lab-04 ↗.

Problem Statement

A developer is working on a game that features characters from the famed fantasy novel, the Lord of the Rings ↗. She’s working on a feature that allows users to create avatars that reflect and represent the characters in the novel. An avatar is a graphical representation of a user or a user’s character. The creation of an avatar is inherently a step-by-step process. You start with a skin tone and additional facial features such as hair, eye brows, mustache, eyeglasses each of which has different styles. This feature is inspired by the highly customized and inclusive feature of creating an avatar in the Snapchat app, which is also named a Bitmoji and shown below.

Snapchat Create a Bitmoji

She started working on three characters: Aragorn, Legolas, and Frodo Baggins. These characters will be represented by three avatars: Knight, Archer, and Ring Flag Bearer respectively. These avatars can be customized with different skin tones, hair colors, hair types, body types and a set of facial features.

Avatars Characters

She started implementing this avatar feature by writing an interface for the Characters and a concrete class for each avatar.

1
2
3
4
5
6
7
public interface Characters {
    public String getName();
    public void setName(String name);
    public Avatar getAvatar();
    public void setAvatar(Avatar avatar);
    public String toString();
}

Flag Bearer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class FlagBearer implements Characters{
    private String name;
    private Avatar avatar;
    
    public FlagBearer(String name){
        if (name == null ) {
            throw new IllegalArgumentException("Character must have a name");
        }
        this.name=name;
        this.avatar=new Avatar(SkinTone.LIGHT, HairType.CURLY, HairColor.BROWN, 
                               BodyType.SKINNY, FacialFeatures.GOATEE);
    }

    public String getName(){
        return this.name;
    }
    
    public Avatar getAvatar(){
        return this.avatar;
    }
    
    public void setName(String name){
        this.name=name;
    }
    
    public void setAvatar(Avatar avatar){
        this.avatar=avatar;
    }

    public String toString(){
        return this.name+" has "+this.avatar.toString();
    }
}

Archer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Archer implements Characters{
    private String name;
    private Avatar avatar;
    
    public Archer(String name){
        if (name == null ) {
            throw new IllegalArgumentException("Character must have a name");
        }
        this.name=name;
        this.avatar = new Avatar(SkinTone.FAIR, HairType.LONG_STRAIGHT, HairColor.BLOND,
                                 BodyType.FIT, FacialFeatures.CLEAN_SHAVEN);
    }

    public String getName(){
        return this.name;
    }

    public Avatar getAvatar(){
        return this.avatar;
    }

    public void setName(String name){
        this.name=name;
    }

    public void setAvatar(Avatar avatar){
        this.avatar=avatar;
    }

    public String toString(){
        return this.name+" has "+this.avatar.toString();
    }
}

Knight:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Knight implements Characters {
   private String name;
   private Avatar avatar;
    
    public Knight(String name){
        if (name == null ) {
            throw new IllegalArgumentException("Character must have a name");
        }
        this.name=name;
        this.avatar=new Avatar(SkinTone.MEDIUM, HairType.LONG_STRAIGHT, HairColor.BLACK,
                               BodyType.MUSCULAR, FacialFeatures.LIGHT_BEARD);
    }

    public String getName(){
        return this.name;
    }
    
    public Avatar getAvatar(){
        return this.avatar;
    }
    
    public void setName(String name){
        this.name=name;
    }
    
    public void setAvatar(Avatar avatar){
        this.avatar=avatar;
    }

    public String toString(){
        return this.name+" has "+this.avatar.toString();
    }
}

Next, she defined a set of enums for skin tones, hair types, hair colors, body types, and facial features. You can think of an enum as a special “class” that represents a group of constants-like final variables.

1
2
3
4
5
6
7
public enum SkinTone {
  FAIR, LIGHT, MEDIUM , DARK;
  @Override
  public String toString() {
    return name().toLowerCase();
  }
}
1
2
3
4
5
6
7
public enum HairType {
  BALD,SHORT,CURLY, LONG_STRAIGHT, LONG_CURLY;
  @Override
  public String toString() {
    return name().toLowerCase();
  }
}
1
2
3
4
5
6
7
public enum HairColor {
  WHITE, BLOND, RED, BROWN, BLACK;
  @Override
  public String toString() {
    return name().toLowerCase();
  }
}
1
2
3
4
5
6
7
public enum FacialFeatures {
  CLEAN_SHAVEN, LIGHT_BEARD, HEAVY_BEARD, GOATEE, MOUSTACHE;
  @Override
  public String toString() {
    return name().toLowerCase();
  }
}
1
2
3
4
5
6
7
public enum BodyType {
  SKINNY, FIT, MUSCULAR, FAT;
  @Override
  public String toString() {
    return name().toLowerCase();
  }
}

Now, she wrote the initial implementation of the Avatar class as shown below. Note that she used constructor overloading for the Avatar class where one constructor contains a set of required parameters, another one with one optional parameter, a third with two optional parameters, and so on, culminating in a constructor with all the optional parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public final class Avatar {

  private final SkinTone skinTone;
  private final HairType hairType;
  private final HairColor hairColor;
  private final BodyType bodyType;
  private final FacialFeatures facialFeatures;

  public Avatar(SkinTone skinTone, HairType hairType, HairColor hairColor, BodyType bodyType, FacialFeatures facialFeatures) {
    this.skinTone = skinTone;
    this.hairType = hairType;
    this.hairColor = hairColor;
    this.bodyType = bodyType;
    this.facialFeatures = facialFeatures;
  }

  public Avatar(SkinTone skinTone, HairType hairType) {
    this(skinTone, hairType, HairColor.BLACK, BodyType.FIT, FacialFeatures.CLEAN_SHAVEN);
  }

  public Avatar(SkinTone skinTone, HairType hairType, HairColor hairColor) {
    this(skinTone, hairType, hairColor, BodyType.FIT, FacialFeatures.CLEAN_SHAVEN);
  }

  public Avatar(SkinTone skinTone, HairType hairType, HairColor hairColor, BodyType bodyType) {
    this(skinTone, hairType, hairColor, bodyType, FacialFeatures.CLEAN_SHAVEN);
  }

  public Avatar(SkinTone skinTone,  BodyType bodyType) {
    this(skinTone, HairType.SHORT, HairColor.BLACK, bodyType, FacialFeatures.CLEAN_SHAVEN);
  }

  public SkinTone getSkinTone() {
    return skinTone;
  }

  public HairType getHairType() {
    return hairType;
  }

  public HairColor getHairColor() {
    return hairColor;
  }

  public BodyType getBodyType() {
    return bodyType;
  }

  public FacialFeatures getFacialFeatures() {
    return facialFeatures;
  }

  @Override
  public String toString() {

    StringBuilder sb = new StringBuilder();
    sb.append(skinTone).append(" skin color");
    if (hairColor != null || hairType != null) {
      sb.append(" with ");
      if (hairColor != null) {
        sb.append(hairColor).append(' ');
      }
      if (hairType != null) {
        sb.append(hairType).append(' ');
      }
      sb.append(hairType != HairType.BALD ? "hair" : " head");
    }
    if (bodyType != null) {
      sb.append(" and a ").append(bodyType).append(" body");
    }
    if (facialFeatures != null) {
      sb.append(" and a ").append(facialFeatures);
    }
    sb.append('.');
    return sb.toString();
  }
}

This implementation is problematic!

This implementation is often known as the telescoping constructor pattern, in which you provide a constructor with only the required parameters, another with a single optional parameter, a third with two optional parameters, and so on. While this implementation works, it does not scale well to large numbers of optional parameters. It is also difficult to remember to the order of required parameters and know what default values are used for optional parameters. It is also hard to read as you’re always left wondering what the order of parameters is, and you need to carefully count parameters to find out which constructor to call. More importantly, this will require adding additional constructors in the future to handle additional combinations and any changes to the order of parameters will break all clients in the three characters Knight, Archer, and FlagBearer.

Enter the Builder design pattern!

The builder pattern is intended to solve this problem in an elegant way allowing us to create different representations of an object and create objects in a step-by-step manner without having to rely on constructor overloading and passing defaults for unused parameters. This pattern is flexible and it is easy to add more parameters to it in the future. This especially important because in this scenario, creating an Avatar, we suspect tp add more parameters in the future. This design pattern feature a common programming syntax called “Method Chaining”, where a method is used to invoke multiple methods on the same object in a single Java statement.

Questions

Question 1: Fix the current implementation of the Avatar class using the builder design pattern and remove the constructors overloading (telescoping constructor).

Question: 2 Complete the code in the CharacterFactory class to have a factory method that creates an Avatar by it’s character type (e.g., Knight, Archer, and FlagBearer) and assigns a name to the character (e.g., Aragorn, Legolas, and Frodo Baggins).

1
2
3
4
5
6
7
public class CharacterFactory {

    // A factory method that returns an object (Archer, FlagBearer, or Knight) by its name
    public static Characters createCharacter(CharacterTypes type, String name){

    }
}

Deliverables and Submission

Extra Task [Optional]

If you are done with this activity, you may enable a continuos integration tool such as CircleCI ↗ to automatically run your JUnit test upon code changes. You may also add more unit tests to increase code coverage. Please embed the badge that shows the status of your build and test (passing/failing) as well as the coverage percentage into your README file (e.g., passing status image and failing status image). Please be sure to fork the repository or push to a remote repository under your own account, so you can enable the integration of CI tools in your own account.

You may refer to the lecture notes from the prerequisite course CPIT-251 on Continuous Integration (CI) and adding a code coverage badge.