The SOLID principles

Some programming guidelines have a wide, general-purpose intention, while some are designed to fix certain specific problems. Thus, before talking about the specific problems, we should review those features that can be applied in many different scenarios and solutions. I mean those principles that should be taken into consideration beyond the type of solution or specific platform to program for.

Moreover, this is where the SOLID principles (and other related problems) come into play. In 2001, Robert Martin published a foundational article on the subject (http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod), in which he picked up a set of principles and guidelines that, in his own words, focus very tightly on dependency management, its inconveniences, and how to solve them.

To explain this further in his words, poor dependency management leads to code that is hard to change, fragile, and non-reusable. Reusability is one the main principles of OOP, along with maintainability (the capacity to change as the project grows: one of the purposes of inheritance).

Overall, there are 11 principles to consider, but they can be divided into three areas:

  • The SOLID principles, which deal with class design
  • The rest of the principles, which are about packages: three of them are about package cohesion and the other three study couplings between packages and how to evaluate the package structure

We're going to start with the SOLID principles, which by extension not only affect the class design, but other architectures as well.

For instance, the application of some of these ideas paved the way for some of the most important modifications in the building of HTML5.

The application of the SRP (Single Responsibility principle), from which the more general design principle of Separation of Concerns is derived, only highlighted the need to totally separate presentation (CSS) from content (HTML) and the subsequent deprecation of some tags (<cite>, <small>, <font>, and so on).

Note

Some of the aforementioned tags are deprecated and not recommended as presentation features, but they are kept in the standard because of their semantic value instead, such as <b>, <i>, <small>, and so on.

This applies to some popular frameworks, such as AngularJS, which was designed not only with the Single Responsibility principle in mind, but also based on the Dependency Inversion principle (the D in SOLID).

The next graphic resumes the five principles' initials and its correspondences:

The SOLID principles

The explanation of every letter in the acronym as expressed in Wikipedia is as follows:

  • S - Single Responsibility Principle: A class should have only a single responsibility (that is, only one potential change in the software's specification should be able to affect the specification of the class). Martin states that this principle is based on the principle of cohesion, previously defined by Tom de Marco in a book named Structured Analysis and Systems Specification and by Meilir Page-Jones in his work The Practical Guide to Structured Systems Design.
  • O - Open/Closed Principle: Software entities should be open for extension, but closed for modification. Bertrand Meyer was the first to propose this principle.
  • L - Liskov Substitution principle: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. Barbara Liskov first stated this.
  • I - Interface Segregation principle: Many client-specific interfaces are better than one general-purpose interface. Robert C. Martin was the first to use and formulate this principle.
  • D - Dependency inversion principle: We should 'Depend on Abstractions'. Do not depend upon concretions. This too is an idea developed by Robert C. Martin.

Single Responsibility principle

For Single Responsibility principle (SRP), the basic statement, in this case, is there should never be more than one reason for a class to change. In this context, responsibility is defined as a reason for change. If, under any circumstance, more than one reason comes up to change the class, the class' responsibilities are multiple and should be redefined.

This is, indeed, one of the most difficult principles to apply properly because as Martin says, conjoining responsibilities is something that we do naturally.

In his book Agile Principles, Patterns, and Practices in C# , Martin proposes a canonical example to show the differences, as follows:

interface Modem
{
  public void dial(String phoneNumber);
  public void hangup();
  public void send(char c);
  public char recv();
}

Given the previous interface, any class implementing this interface has two responsibilities: the connection management and the communication itself. Such responsibilities can be used from the different parts of an application, which, in turn, might change as well.

Instead of this code structure, Martin proposes a different diagram:

Single Responsibility principle

However, one wonders, should these two responsibilities be separated? It only depends on application changes. To be precise, the key here is to know whether changes in the application affect the signature of connection functions. If they do, we should separate both; otherwise, there's no need for separation because we would then create needless complexity.

So, overall, reason to change is the key, but keep in mind that a reason to change is applicable only if changes occur.

In other situations, there might be reasons to keep distinct responsibilities together as long as they are closely related to the business definitions or have to do with hardware requirements of the operating system.

An example

Let's imagine we need to create a simple Windows Forms application (we pick this model for simplicity in order to avoid unnecessary XAML), which has the ability to offer the user a few cars (actually, just three different brands), and the application should show maximum speed and a photo for the selected car.

Later on, we can derive from the class hierarchy to create different versions that are able to cover distinct characteristics, specific of business models, or legal conditions, among others.

So, the first step is to represent the user interface that will cover the requirements, mentioned previously, according to the indications. I came up with a very simple Windows form, as shown in the following screenshot:

An example

We're dealing with three (or more) brands, and optionally, we have a place to display the maximum speed value. We also included a button for acceleration so that we can verify that the car never goes faster than its maximum speed limit. Finally, the photo will remind us about the car we're dealing with.

So, we plan to define a class named SportCar, in which we will abstract the required elements to be managed from the UI, and to make things clearer, we start by creating an interface, ISportCar, which states the requisites.

We can use the Class Diagram tool to create an interface that defines four properties and one method: Brand, MaxSpeed, Photo, Speed, and Accelerate (which will change the Speed property from the user interface). So, the final code is as follows:

interfaceISportCar
{
  bool Accelerate();
  System.Drawing.Bitmap Photo { get; }
  string Brand { get; }
  int Speed { get; }
  int MaxSpeed { get; }
}

Using the Class Diagram tool, we can create a SportCar class and link it to the interface so it declares the dependency. Later on, with the basic class declaration created by the IDE, we can move on to the source code and indicate the Implement Interface option to have the class with the interface implemented for us.

A few touches for the sake of simplicity can have us end up with the following initial code:

public class SportsCar : ISportCar
{
  public string Brand { get; }
  public int MaxSpeed { get; }
  public Bitmap Photo { get; }
  public int Speed { get;privateset; }
  public virtual bool Accelerate()
  {
    throw new NotImplementedException();
  }
}

Observe that all properties are read-only, since all except one should be established at creation time, and the only method that changes (Speed) must only vary using the Accelerate method (declared as virtual in order to allow further inheritance). This method returns a Boolean value to indicate the limit conditions: MaxSpeed exceeded. This is why it's declared a private set.

On the graphic side, our (now modified) diagram should reveal dependencies and members of both code fragments:

An example

So, at first, the class has the sole responsibility of managing the state of a SportCar instance of the class. This implies business logic: a Ferrari looks like a Ferrari, not like a BMW, and each one has its own properties (MaxSpeed and Speed in this case). Nothing related to the user interface or storing state, among other things, should be considered here.

Next, we need a constructor that enforces some of the principles mentioned earlier. It should resolve all the immutable properties; so, when the class is created, they are assigned the proper value.

Here, we face another problem: how does our class know about the possible brands available? There are several approaches here, but a simple one would be to declare an internal array defining the allowed brands and have the constructor check whether the brand suggested in the construction is one of the brands our class can manage.

Note that I have included three simple pictures corresponding to the three brands inside the application's resource file. This is a dependency. If a fourth brand needs to be considered, we should change the constructor to supply this additional functionality, but for the sake of simplicity, let's assume that no changes in the business logic of the number of cars will happen for the moment.

With all this in mind, we will add the following code to our class:

string[] availableBrands = new string[] { "Ferrari", "Mercedes", "BMW" };
public SportsCar(string brand)
{
  if (!availableBrands.Contains(brand)) return;
  else Brand = brand;
  switch (brand)
  {
    case "Ferrari":
      MaxSpeed = 350;
      Photo = Properties.Resources.Ferrari;
    break;
    case "Mercedes":
      MaxSpeed = 300;
      Photo = Properties.Resources.Mercedes;
    break;
    case "BMW":
      MaxSpeed = 270;
      Photo = Properties.Resources.BMW;
    break;
  }
}

With this, we have an operational (although incomplete) version of our class. Now, in the user interface, we should declare a variable of the SportCar class and instantiate it every time the user changes the brand using the cboPickUpCar ComboBox.

Actually, we also need to update the UI once the car is instantiated so that it reflects the properties of the car (its state). And it should be consistent with the properties of every brand available.

This simple code does the trick:

SportsCar theCar;
private void cboPickUpCar_SelectedIndexChanged(object sender, EventArgs e)
{
  theCar = new SportsCar(cboPickUpCar.Text);
  // refresh car's properties
  txtMaxSpeed.Text = theCar.MaxSpeed.ToString();
  pbPhoto.Image = theCar.Photo;
}

Now, we have a first version that works properly, but our class needs to have the ability to change the Speed property. So we add some code to the Accelerate method:

public virtual bool Accelerate()
{
  bool speedExceeded = Speed + SpeedIncr > MaxSpeed;
  Speed = (speedExceeded) ? Speed: Speed + SpeedIncr;
  return speedExceeded;
}

And that's it. We should now reflect these changes in the UI, which is pretty straightforward:

private void btnAccelerate_Click(object sender, EventArgs e)
{
  theCar.Accelerate();
  updateUI();
}
private void updateUI()
{
  txtSpeed.Text = theCar.Speed.ToString();
}

The final result should work as expected (refer to the screenshot). You can pick from the different brands available, and every new selection provokes a new instantiation of the SportCar class.

We can see all the properties at runtime, and the only mutable property (Speed) is changed exclusively from the Accelerate method, which now has a unique responsibility.

However, since this responsibility implies business logic, it also checks whether an attempt to exceed the allowed speed has taken place and avoids a case inspecting the possible value of an increase in the speed (we have assumed a constant value for that speed in the initial declarations of the class). You should see an output like the following:

An example

Now, let's consider some possible situations that arise when changes are proposed. This is when the next principle comes into action, and it deals with how to manage requisites when new conditions arise.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.37.126