1
\$\begingroup\$

I want to implement classes in a project that will support the loading of configuration data from a JSON file. The JSON file is in the following form. Please be as critical as you can be. I'm a junior developer and I want to learn how to write clean code.

[
  {
    "teamIdentity": "team01",
    "projectIdentities": [
      {
        "projectIdentity": "project01"
      },
      {
        "projectIdentity": "project02"
      },
      {
        "projectIdentity": "project03"
      }
    ]
  },
  {
    "teamIdentity": "team02",
    "projectIdentities": [
      {
        "projectIdentity": "project04"
      },
      {
        "projectIdentity": "project05"
      },
      {
        "projectIdentity": "project06"
      }
    ]
  },
  {
    "teamIdentity": "team03",
    "projectIdentities": [
      {
        "projectIdentity": "project07"
      },
      {
        "projectIdentity": "project08"
      },
      {
        "projectIdentity": "project09"
      }
    ]
  }
]

Here is the unit test written in ScalaTest:

class JsonConfigTest extends FlatSpec {

  "JsonConfig" must "load the default JSON file" in {
    val config = JsonConfigFactory.newInstance
    assert(config != null)
  }

  it must "throw an exception when the JSON file does not exist" in {
    assertThrows[IllegalArgumentException] {
      JsonConfigFactory.newInstance("absent-file.json")
    }
  }

  it must "successfully validate the JSON file" in {
    pending
  }

  it must "throw an exception if validation for the JSON file fails" in {
    pending
  }

  it must "return a collection of TeamData objects" in {
    pending
  }
}

This is the interface for Config

/**
 * The interface for configuration.
 */
public interface Config {

    /**
     * Returns the configuration data for all teams.
     *
     * @return {@code List<TeamData>}
     */
    List<TeamData> getAllTeamData();
}

I then proceed to write the JsonConfigFactory class.

/**
 * The factory for JsonConfig.
 */
public final class JsonConfigFactory {

    /**
     * Creates a default instance of JsonConfig.
     *
     * @return {@code JsonConfig}
     */
    public static JsonConfig newInstance() {
        return new JsonConfig();
    }

    /**
     * Creates a instance of JsonConfg with the filename provided.
     *
     * @param filename The name of the file that contains the configuration data.
     * @return {@code JsonConfig}
     */
    public static JsonConfig newInstance(String filename) {
        return new JsonConfig(filename);
    }
}

This is the concrete implementation of Config called JsonConfig that will support the loading of the JSON file.

/**
 * The JSON configuration.
 */
public final class JsonConfig implements Config {

    /**
     * The root JSON node.
     */
    private JsonNode rootConfigNode;

    /**
     * Default constructor.
     */
    public JsonConfig() {
        this.rootConfigNode = getRootConfigNode(JsonConfigUtils.JSON_CONFIG_FILE_NAME);
    }

    /**
     * Constructor that accepts a filename that contains the configuration data.
     *
     * @param filename The filename.
     */
    public JsonConfig(String filename) {
        this.rootConfigNode = getRootConfigNode(filename);
    }

    @Override
    public List<TeamData> getAllTeamData() {
        var teamData = new ArrayList<TeamData>();
        final var teamConfigNodes = rootConfigNode.elements();
        teamConfigNodes.forEachRemaining(node -> teamData.add(getTeamDataFromConfigNode(node)));
        return teamData;
    }

    /**
     * Returns the TeamData from a JsonNode.
     *
     * @param teamConfigNode The node containing the team data.
     * @return {@code TeamData}
     */
    private TeamData getTeamDataFromConfigNode(JsonNode teamConfigNode) {
        return JsonTeamDataFactory.newInstance(teamConfigNode);
    }

    /**
     * Returns the root json node from the file.
     *
     * @param filename The name of the file.
     * @return {@code JsonNode}
     */
    private JsonNode getRootConfigNode(String filename) {
        try {
            final var inputStream = getClass().getClassLoader().getResourceAsStream(filename);
            return new ObjectMapper().readValue(inputStream, JsonNode.class);
        } catch (IOException e) {
            throw new IllegalArgumentException();
        }
    }
}

This is the helper class for common strings

/**
 * The JSON configuration utils.
 */
final class JsonConfigUtils {

    /**
     * The name of the default JSON configuration file.
     */
    static final String JSON_CONFIG_FILE_NAME = "config.json";

    /**
     * The name of the team identity field.
     */
    static final String TEAM_IDENTITY_FIELD = "teamIdentity";

    /**
     * The name of the project identities field.
     */
    static final String PROJECT_IDENTITIES_FIELD = "projectIdentities";

    /**
     * The name of the team project identity field.
     */
    static final String PROJECT_IDENTITY_FIELD = "projectIdentity";
}

This is the interface for TeamData.

/**
 * The interface for TeamData.
 */
public interface TeamData {

    /**
     * Returns the ID of the team.
     *
     * @return {@code String}
     */
    String getTeamIdentity();

    /**
     * Returns the IDs of the projects.
     *
     * @return {@code List<String>}
     */
    List<String> getProjectIdentities();
}

This is a factory for JsonTeamData.

/**
 * Factory for JsonTeamData.
 */
public class JsonTeamDataFactory {

    /**
     * Returns a new instance of JsonTeamData.
     *
     * @param teamDataNode The JSON node.
     * @return {@code JsonTeamData}
     */
    public static JsonTeamData newInstance(JsonNode teamDataNode) {
        return new JsonTeamData(teamDataNode);
    }
}

This is the concrete implementation of TeamData called JsonTeamData that will support the loading of JSON.

/**
 * The JSON team data.
 */
public final class JsonTeamData implements TeamData {

    /**
     * The JSON node containing the team data.
     */
    private JsonNode teamDataNode;

    /**
     * The team identity.
     */
    private String teamIdentity;

    /**
     * The collection of project identities.
     */
    private List<String> projectIdentities;

    /**
     * Constructor for JsonTeamData.
     *
     * @param teamDataNode The JSON node.
     */
    public JsonTeamData(JsonNode teamDataNode) {
        this.teamDataNode = teamDataNode;
        initTeamIdentity();
        initProjectIdentities();
    }

    @Override
    public String getTeamIdentity() {
        return teamIdentity;
    }

    @Override
    public List<String> getProjectIdentities() {
        return projectIdentities;
    }

    /**
     * Initializes the team identity.
     */
    private void initTeamIdentity() {
        this.teamIdentity = teamDataNode.get(JsonConfigUtils.TEAM_IDENTITY_FIELD).asText();
    }

    /**
     * Initializes the project identities.
     */
    private void initProjectIdentities() {
        var projectIds = new ArrayList<String>();
        final var projectIdsNode = teamDataNode.findValues(JsonConfigUtils.PROJECT_IDENTITIES_FIELD);
        projectIdsNode.forEach(node -> {
            final var projectIdentityNodes = node.elements();
            projectIdentityNodes.forEachRemaining(projectIdentity -> {
                projectIds.add(projectIdentity.get(JsonConfigUtils.PROJECT_IDENTITY_FIELD).asText());
            });
        });
        this.projectIdentities = projectIds;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.