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 acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Browse other questions tagged or ask your own question.