The last of the SOLID principles is based on two statements, that Wikipedia states in this form:
As for the first statement, we should clarify what we understand by high-level and low-level modules. The terminology is related to the importance of the actions performed by the module.
Let's put it simply: if a module holds the business logic of a Customers
class, and another includes the format that a list of the Customers
class uses in a report, the first one would be high-class and the second would be low-class.
The second statement speaks by itself. If an abstraction depends on details, the usage as a definition contract is compromised.
In the case of our sample, we still have some code that will not grow appropriately: the SportsCar
creation method depends much on what the user writes in the ComboBox. There are several situations that could show this inconvenience: writing the wrong name in the brand selection procedure, adding future new brands, and so on. There is some boilerplate code in the UI that we can improve.
Without pretending that the sample is perfect (at all), the creation procedure can be extracted from the UI and delegated to another class (CarFactory
) that would be responsible for calling the appropriate constructor depending on the brand. (We'll see that this technique is actually implemented using one of the design patterns we'll study later on.)
In this way, the responsibility of calling the proper constructor would be on CarFactory
, and additional brands can be added more easily.
In addition, our SportsCar
class will now exclusively take care of its state and business logic related to the state and not the details of Photo
associations or MaxSpeed
values, which seem adequate for a factory.
So, we will now have a new class (located in the same file as the SportsCar
file), containing these details:
public class CarFactory { SportsCar carInstance; public SportsCar CreateCar(string car) { switch (car) { case "Ferrari": carInstance = new SportsCar(car); carInstance.MaxSpeed = 230; carInstance.Photo = Properties.Resources.BMW; break; case "BMW": carInstance = new SportsCar(car); carInstance.MaxSpeed = 180; carInstance.Photo = Properties.Resources.BMW; break; case "Mercedes": carInstance = new SportsCarWithN(car); carInstance.MaxSpeed = 200; carInstance.Photo = Properties.Resources.Mercedes; break; default: break; } return carInstance; } }
With this new version, the SportsCar
class is reduced to a minimum: it declares constants, its event, its state (properties), and the only action required (Accelerate
). The rest is in the hands of the CarFactory
class.
The user interface is also simplified in the creation method, since it doesn't need to know which brand the user selected in order to call either constructor; it simply calls the constructor inside CarFactory
and checks the result of the process in order to assign the event handlers required to show the car's notifications:
private void cboPickUpCar_SelectedIndexChanged(object sender, EventArgs e) { var factory = new CarFactory(); theCar = factory.CreateCar(cboPickUpCar.Text); // Event common to all cars theCar.LegalLimitCondition += TheCar_LegalLimitCondition; // Event specific to cars of type SportsCarWithN if (theCar is SportsCarWithN) { ((SportsCarWithN)theCar).SpeedLimit += TheCar_SpeedLimit; } // refresh car's properties txtMaxSpeed.Text = theCar.MaxSpeed.ToString(); pbPhoto.Image = theCar.Photo; updateUI(); }
The runtime behavior is just the same as earlier. The difference is that with this decoupling of components, maintenance and growing are much easier.
Let's imagine that a change happens and the application now has to deal with a new type of brand: Ford, which also incorporates SpeedLimit
notifications.
The only work to do is to add a picture of a Ford (a Ford GT, not to detract from the other cases…) and retouch CarFactory
to add the new case structure and its values:
case"Ford": carInstance = new SportsCarWithN(car); carInstance.MaxSpeed = 210; carInstance.Photo = Properties.Resources.Ford; break;
In the UI, only one thing is required: adding the new Ford
string to the selection ComboBox, and it's ready. Now, we'll be offered the new brand, and when we select it, the behavior will be as expected:
Generally speaking, there are many ways in which the DIP principle can lead to a solution. One of them is through a dependency container, which is a component, which serves or provides you with some code, injecting it when required.
Some popular dependency containers for C# are Unity and Ninject, to name just a couple. In the code, you instruct this component to register certain classes of your application; so, later on, when you need an instance of one of them, it is served to your code automatically.
Other frameworks implement this principle as well, even if they're not purely object oriented. This is the case with AngularJS, in which, when you create a controller that requires access to a service, you ask for the service in the controller's function declaration, and the internal DI system of Angular serves a singleton instance of the service without the intervention of the client's code.
3.135.201.217