Skip to main content

Solid Design Principles

Single Responsibility

  • A class should do only 1 thing. If it is not then split it.

Open Closed

A class is open for extension, closed for modification. Not to be tempted by quick-fixes and flags.

Liskov's Substitution

The program should not break if a type T is replaced by its subtype S. This principle comes with a set of guidelines that are to be followed when sub-typing:

TODO: Covariance, Invariants, Preconditions, Postconditions

Inteface Seggregation

No code should be forced to depend on methods it does not use. Split the interface if it is asking for too many things.

Dependency Inversion

It feels natural to build systems where a higher level component depends on a lower level component.

  • Higher level: Components that implement the core bussiness logic. For example a WeatherService that returns Temperature based on Geo-location.
  • Lower level: These are external dependencies. For example an ApacheClient.
class WeatherService {
ApacheClient apacheClient;

public WeatherService(ApacheClient apacheClient) {
this.apacheClient = apacheClient;
}

public Temperature temperatureByCity(String city) {
Response geoResponse = apacheClient.get("http://maps.api?city=" + city);
Location geoCode = str2Location(geoResponse.body());

Response temperatureResponse = apacheClient.get("http://weather.provider?latitude=" + Location.lat + "&longitude=" + Location.long);
return str2Temperature(temperatureResponse.body());
}
}

A breaking change in ApacheClient such a change in method signature of get() would require a change in WeatherService. Same situation happens if we plan on switching to a different client.

We can invert this dependency and make ApacheClient depend on WeatherService.

WeatherService will specify the contract that needs to be satisfied by HttpClients. This contract is represented by an interface and ideally it should be declared within the same package as WeatherService.

public interface HttpClient {
Response get(Request req);
}

class WeatherService {
HttpClient httpClient;

public WeatherService(HttpClient httpClient) {
this.httpClient = httpClient;
}

public Temperature temperatureByCity(String city) {
Response geoResponse = httpClient.get(new Request("http://maps.api?city=" + city));
Location geoCode = str2Location(geoResponse.body());

Response temperatureResponse = httpClient.get(new Request("http://weather.provider?latitude=" + Location.lat + "&longitude=" + Location.long));
return str2Temperature(temperatureResponse.body());
}
}

ApacheClient would implement this contract.

class ApacheHttpClient implements HttpClient {

ApacheClient apacheClient;

public ApacheHttpClient(ApacheClient apacheClient) {
this.apacheClient = apacheClient;
}

Response get(Request req) {
String res = apacheClient.get(req.Url());
return new Response(res);
}
}
  • Future changes to httpClient will not require changes in WeatherService
  • We can replace the client altogether to different implementation such as URLconnection and it would not require any change in our core business class.
  • WeatherService is not new()ing ApacheHttpClient, this dependency is being injected. A programming technique known as dependency injection in this case constructor injection to be precise.