C# 7 and .NET Core 2.0 Blueprints
上QQ阅读APP看书,第一时间看更新

Open/closed principle

Previously, we had a look at the single responsibility principle. Hand in hand with this is the open/closed principle.

Bertrand Meyer stated that software entities (classes, modules, functions, and so on):

  • Should be open for extension
  • Should be closed for modification

What exactly does this mean? Let's take the PlayerStatistics class as an example. Inside this class, you know that we have a method to calculate the strike rate of a particular player. This is included in the class because it inherits from the Statistics abstract class. That is correct, but the fact that the CalculateStrikeRate(Player player) method caters for two player types (all-rounders and batsmen) is already a hint of a problem.

Let's assume that we have introduced new player types—different bowler types (for example, fast bowlers and spin bowlers). In order for us to accommodate the new player type, we must change the code in the CalculateStrikeRate() method.

What if we wanted to pass through a collection of batsmen to calculate the average strike rate between all of them? We would need to modify the CalculateStrikeRate() method again to accommodate this. As time goes by and the complexities increase, it will become very difficult to keep on catering for different player types that need the strike rate calculation. This means that our CalculateStrikeRate() method is open for modification and closed for extension. This is in contravention of the principles stated previously in the bullet list.

So, what can we do to fix this? In truth, we are already halfway there. Start by creating a new Bowler class in the Classes folder:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class Bowler : Player, IBowler 
    { 
        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 
 
        #region IBowler 
        public double BowlerSpeed { get; set; } 
        public string BowlerType { get; set; }  
        public int BowlerBallsBowled { get; set; } 
        public int BowlerMaidens { get; set; } 
        public int BowlerWickets { get; set; } 
        public double BowlerEconomy => BowlerRunsConceded / 
BowlerOversBowled; public int BowlerRunsConceded { get; set; } public int BowlerOversBowled { get; set; } #endregion } }

You can see how easy it is to construct new player types—we have only to tell the class that it needs to inherit the Player abstract class and implement the IBowler interface.

Next, we need to create new player statistics classes—namely, BatsmanStatistics, BowlerStatistics, and AllRounderStatistics. The code for the BatsmanStatistics class will look as follows:

using cricketScoreTrack.BaseClasses; 
using System; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class BatsmanStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 
 
        public override double CalculateStrikeRate(Player player) 
        { 
            if (player is Batsman batsman) 
            { 
                return (batsman.BatsmanRuns * 100) / 
batsman.BatsmanBallsFaced; } else throw new ArgumentException("Incorrect argument
supplied"); } } }

Next, we add the AllRounderStatistics class:

using cricketScoreTrack.BaseClasses; 
using System; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class AllRounderStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 
 
        public override double CalculateStrikeRate(Player player) 
        { 
            if (player is AllRounder allrounder) 
            { 
                return (allrounder.BowlerBallsBowled / 
allrounder.BowlerWickets); } else throw new ArgumentException("Incorrect argument
supplied"); } } }

Lastly, we add the new player type statistics class called BowlerStatistics:

using cricketScoreTrack.BaseClasses; 
using System; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class BowlerStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 
 
        public override double CalculateStrikeRate(Player player) 
        { 
            if (player is Bowler bowler) 
            { 
                return (bowler.BowlerBallsBowled / 
bowler.BowlerWickets); } else throw new ArgumentException("Incorrect argument
supplied"); } } }

Moving the responsibility of calculating the strike rates for all players away from the PlayerStatistics class makes our code cleaner and more robust. In fact, the PlayerStatistics class is all but obsolete.

By adding another player type, we were able to easily define the logic of this new player by implementing the correct interface. Our code is smaller and easier to maintain. We can see this by comparing the previous code for CalculateStrikeRate() with the new code we wrote.

To illustrate more clearly, take a look at the following code:

public override double CalculateStrikeRate(Player player) 
{             
    switch (player) 
    { 
        case AllRounder allrounder: 
            return (allrounder.BowlerBallsBowled / 
allrounder.BowlerWickets); case Batsman batsman: return (batsman.BatsmanRuns * 100) /
batsman.BatsmanBallsFaced; case Bowler bowler: return (bowler.BowlerBallsBowled / bowler.BowlerWickets); default: throw new ArgumentException("Incorrect argument
supplied"); } }

The preceding code is much more complex and less maintainable than the following:

public override double CalculateStrikeRate(Player player) 
{ 
    if (player is Bowler bowler) 
    { 
        return (bowler.BowlerBallsBowled / bowler.BowlerWickets); 
    } 
    else 
        throw new ArgumentException("Incorrect argument supplied"); 
} 

The benefit of creating a BowlerStatistics class, for example, is that you know that throughout the class we are only dealing with a bowler and nothing else...a single responsibility that is open for extension without having to modify the code.