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
WeatherServicethat 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
httpClientwill not require changes inWeatherService - We can replace the client altogether to different implementation such as
URLconnectionand it would not require any change in our core business class. WeatherServiceis notnew()ingApacheHttpClient, this dependency is being injected. A programming technique known asdependency injectionin this caseconstructor injectionto be precise.