Photo by James Pond on Unsplash

Things to consider when designing objects

Milan Brankovic
6 min readMar 9, 2021

It is very simple to learn how to code, but it is very hard to make the code readable/understandable, clean and robust. Let's try to mention some principles in making this easier.

#1 Inject dependencies and configuration values as constructor arguments

public interface Worker {
public void work();
}

public class ConstructionWorker implements Worker {
private List<Tool> tools;

public ConstructionWorker(final List<Tool> tools) {
this.tools = tools;
}

@Override
public void work() {
// use tools
}
}

#2 Keep configuration properties together

before:

public class ApiClient {
private String username;
private String password;

public ApiClient(final String username, final String password) {
this.username = username;
this.password = password;
}
}

after:

class Credentials {
private String username;
private String password;
// constructor and getters}public class ApiClient {
private Credentials credentials;

public ApiClient(final Credentials credentials) {
this.credentials = credentials;
}
}

#3 Inject what you need (nothing more/nothing less)

#4 All constructor arguments should be required

before:

class Credentials {
private String username;
private String password;
// constructor and getters}public class ApiClient {
private Optional<Credentials> credentials;
private RestTemplate restTemplate;

public ApiClient(final Optional<Credentials> credentials,
final RestTemplate restTemplate) {
this.credentials = credentials;
this.restTemplate = restTemplate;
}
public String fetchData() {
// if credentials are present add to HttpHeader
// perform work
}
}

after:

class Credentials {
private String username;
private String password;
// constructor and getters}public class ApiClient {
private Credentials credentials;
private RestTemplate restTemplate;

public ApiClient(final Credentials credentials,
final RestTemplate restTemplate) {
this.credentials = credentials;
this.restTemplate = restTemplate;
}
public String fetchData() {
// add credentials to HttpHeader
// perform work
}
}

#5 Only use constructor injection

#6 Turn static dependency into object dependency

before:

class StaticClass {
public static void staticMethod() {}
}
public class MyClass {
// constructor and getters
public String doWork() {
StaticClass.staticMethod();
// do some other work
}
}

after:

class StaticClass {
public static void staticMethod() {}
}
public class MyClass {
private StaticClass dependency;
// constructor and getters public String doWork() {
this.dependency.staticMethod();
// do some other work
}
}

#7 Turn complicated functions into object dependencies

before:

public class Resource {
// constructor and getters
public String getDataById(final String id) {
// fetch data by id
return jsonEncode(data);
}
public String jsonEncode(final Object data) {
// compicated work
}
}

after:

public interface Encoder {
public String encode(final Object data);
}
public class JsonEncoder implementes Encoder {
@Override
public String encode(final Object data) {
// encode as json
}
}

public class Resource {
private final Encoder encoder;

// constructor and getters
public String getDataById(final String id) {
// fetch data by id
return encoder.encode(data);
}
}

#8 Make system calls explicit

before:

public class Service {
// constructor and getters
public List<Object> getData() {
final Calendar calendar = new GregorianCalendar(pdt);
return findDataInWeek(calendar.get(Calendar.WEEK_OF_YEAR))
}
public List<Object> findDataInWeek(int weekNum) {
}
}

after:

public class Service {
private Calendar calendar;
// constructor and getters public List<Object> getData() {
return findDataInWeek(calendar.get(Calendar.WEEK_OF_YEAR))
}
public List<Object> findDataInWeek(int weekNum) {
}
}

#9 Task related data should be passed as method argument

#10 Prevent changing behavior of a service after instantiation

public class Service {    private boolean shouldReportErrors = true;    // constructor and getters
// only setter for shouldReportErrors
public String doWork() {
// ...
if (shouldReportErrors) { <- changes behavior based on value
// report error(s)
}
// ...
}
}

#11 Constructor should be used only for properties assignment

#12 Throw invalid argument exception when constructing object if an argument is invalid

public class Fighter {
private final int rank;
public Fighter(final int rank) {
if (rank < 0) {
throw new InvalidArgumentException('Fighter rank should be positive');
}
this.rank = rank;
}
}

#13 Don't use custom exception class for invalid argument when constructing an object

#14 Define as little as possible service entry points

A & B may produce different results if both have exposed the same operation for fetching common data through A1 & B1

#15 Extract object when validation is performed on multiple places

before:

public class FileDownloadService {
private final String protocol;
private final String host;
private final String port;
public FileDownloadService(final String protocol, final String host, final String port) {
if (!isProtocolValid(protocol)) {
throw new InvalidArgumentException('...');
}
if (!isHostValid(host)) {
throw new InvalidArgumentException('...');
}
if (!isPortValid(port)) {
throw new InvalidArgumentException('...');
}
this.protocol = protocol;
this.host = host;
this.port = port;
}
}

after:

public class FileServer {
private final String protocol;
private final String host;
private final String port;
public FileServer(final String protocol, final String host, final String port) {
if (!isProtocolValid(protocol)) {
throw new InvalidArgumentException('...');
}
if (!isHostValid(host)) {
throw new InvalidArgumentException('...');
}
if (!isPortValid(port)) {
throw new InvalidArgumentException('...');
}
this.protocol = protocol;
this.host = host;
this.port = port;
}
}
public class FileDownloadService {
private final FileServer fileServer;
public FileDownloadService(final FileServer fileServer) {
this.fileServer = fileServer;
}
}

#16 Extract new object to represent composite value

before:

public class Product {
private final BigDecimal amount;
private final String currency;
// other properties
public Product(final BigDecimal amount, final String currency, // other properties) {

}
}

after:

public class Money {
private final BigDecimal amount;
private final String currency;
}
public class Product {
private final Money money;
// other properties
public Product(final Money money, // other properties) {

}
}

#17 Use assertions to validate constructor arguments

#18 Pass dependencies as method arguments instead of injecting them

before:

public interface Encoder {
public String encode(final Object data);
}
public class JsonEncoder implementes Encoder {
@Override
public String encode(final Object data) {
// encode as json
}
}
public class Resource {
private final Encoder encoder;

// constructor and getters
public String encodeData(final Object data) {
// fetch data by id
return encoder.encode(data);
}
}

after:

public interface Encoder {
public String encode(final Object data);
}
public class JsonEncoder implementes Encoder {
@Override
public String encode(final Object data) {
// encode as json
}
}
public class Resource {

// constructor and getters
public String encodeData(final Encoder encoder, final Object data) {
// fetch data by id
return encoder.encode(data);
}
}

#19 Create methods to construct object from primitive type

public class LocalDate {
public static parse(final String date) { ... }
}

see more: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/LocalDate.html

#20 Use private constructor to enforce constraints

see details of: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/LocalDate.html

#21 Don't test constructors

#22 Use replaceable, anonymous and immutable values when creating value objects

#23 Replace values instead of modifying them

see details of: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/LocalDate.html#minus(long,java.time.temporal.TemporalUnit)

#24 On a mutable object modifier methods should be command methods

public class Fighter {
private final int rank;
private final Position position
public Fighter(final int rank, final Position position) {
if (rank < 0) {
throw new InvalidArgumentException('Fighter rank should be positive');
}
this.rank = rank;
this.position = position;
}
public void move(final Direction direction) {
this.position = this.position.move(direction);
}
public Position currentPosition() {
return this.position;
}
}

#25 Properties and methods should have declarative names

#26 When comparing two objects they should be fully compared (all properties)

#27 When calling modifier on an object it should leave it in a valid state

public class Money {
private final BigDecimal amount;
private final String currency;
// constructor and getters public Money withdraw(final BigDecimal amount) {
if (this.amount - amount < 0) {
// throw InvalidWithdrawalException
}

// process withdrawal
}
}

#28 When calling modifier on an object have a precondition check

#29 When throwing exception on a check failure add detailed message

#30 Query methods should return single-type value

#31 Object should have proper boundaries set

before:

public class Money {
private final BigDecimal amount;
private final String currency;
}
public class Product {
public Money amount;
public boolean shouldApplyDiscount() { // ...
}
public Money fixedDiscountAmount() { // ...
}
}
amount = new Money();
if (product.shouldApplyDiscount()) {
amount = product.getAmount().substract(product.fixedDiscountAmount());
}

after:

public class Money {
private final BigDecimal amount;
private final String currency;
}
public class Product {
public Money amount;
private boolean shouldApplyDiscount() { // ...
}
private Percentage discount() { // ...
}
public Money getNetAmount() {
if (shouldApplyDiscount()) {
return this.discount().applyTo(this.amount);
}
}
}
amount = product.getNetAmount();

#32 Query methods should not issue commands

#33 Use queries to collect information and commands to take next steps

#34 Use mocks to verify calls to command methods

#35 Separate read from write objects

#36 Compose abstraction to achieve complicated behavior

public interface Encoder {
public List<String> encode(final Object data);
}
public class JsonEncoder implementes Encoder {
@Override
public List<String> encode(final Object data) {
// encode as json
}
}
public class MultipleTypeEncoder implementes Encoder {
private final List<Encoder> encoders;
// constructor and getters @Override
public List<String> encode(final Object data) {
final List<String> result = new ArrayList<>();
for(final Encoder encoder : encoders) {
result.add(encoder.encode(data));
}
return result;
}
}

#37 Don't use inheritance to change an object behavior

#38 Mark classes as final whenever possible

#39 Mark method as private whenever possible

#40 Mark properties as private as default

--

--

No responses yet