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 inWeatherService
- 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 notnew()
ingApacheHttpClient
, this dependency is being injected. A programming technique known asdependency injection
in this caseconstructor injection
to be precise.