2. Building Quality Object-Oriented Code

Overview

In this chapter, you will learn how to simplify complex logic using Object-Oriented Programming (OOP). You will start by creating classes and objects, before exploring the four pillars of OOP. You will then learn about some of the best practices in coding, known as the SOLID principles, and see how you can use C# 10 features to write effective code guided by these principles. By the end of this chapter, you will be able to write clean code using object-oriented design with C#.

Introduction

How do people write software that is still maintainable even after many decades? What is the best way to model software around real-world concepts? The answer to both questions is Object Oriented Programming (OOP). OOP is a widely used paradigm in professional programming and is especially useful in enterprise settings.

OOP can be thought of as a bridge that connects real-world concepts and source code. A cat, for example, has certain defining properties, such as age, fur color, eye color, and name. The weather can be described using factors such as temperature and humidity. Both of these are real-world concepts that humans have identified and defined over time. In OOP, classes are what help in defining the logic of a program. When assigning concrete values to the properties of these classes, the result is an object. For example, using OOP, you can define a class for representing a room in a house, and then assign values to its properties (color and area) to create an object of that class.

In Chapter 1, Hello C#, you learned how to use C# to write basic programs. In this chapter, you will see how you can design your code by implementing OOP concepts and using C# at its best.

Classes and Objects

A class is like a blueprint that describes a concept. An object, on the other hand, is the result you get after the application of this blueprint. For example, weather can be a class, and 25 degrees and cloudless could refer to an object of this class. Similarly, you can have a class named Dog, while a four-year-old Spaniel can represent an object of the Dog class.

Declaring a class in C# is simple. It starts with the class keyword, followed by the class name and a pair of curly braces. To define a class named Dog, you can write the following code:

class Dog

{

}

Right now, this class is just an empty skeleton. However, it can still be used to create objects by using the new keyword, as follows:

Dog dog = new Dog();

This creates an object named dog. Currently, the object is an empty shell, as it lacks properties. You will see in an upcoming section how to define properties for classes, but first, you will explore constructors.

Constructors

In C#, constructors are functions used to create new objects. You can also use them to set the initial values of an object. Like any function, a constructor has a name, takes arguments, and can be overloaded. A class must have at least one constructor, but if needed, it can have multiple constructors with different arguments. Even if you do not explicitly define a single constructor, a class will still have a default constructor–one that does not take any arguments or perform any actions but simply assigns memory to the newly created object and its fields.

Consider the following snippet, where a constructor for the Dog class is being declared:

// Within a class named Dog

public class Dog

{

  // Constructor

  public Dog()

  {

    Console.WriteLine("A Dog object has been created");

  }

}

Note

You can find the code used for this example at https://packt.link/H2lUF. You can find the usage of the code at https://packt.link/4WoSX.

If a method has the same name as the class and does not provide a return type, it is a constructor. Here, the snippet of the code is within a class named Dog. So, the constructor is within the specified line of code. Note that by defining this constructor explicitly, you hide the default constructor. If there is one or more such custom constructors, you will no longer be able to use a default constructor. Once the new constructor is called, you should see this message printed in the console: "A Dog object has been created".

Fields and Class Members

You already know what a variable is: it has a type, a name, and a value, as you saw in Chapter 1, Hello C#. Variables can also exist in the class scope, and such a variable is called a field. Declaring a field is as simple as declaring a local variable. The only difference is the addition of a keyword at the start, which is the access modifier. For example, you can declare a field within the Dog class with the public access modifier, as follows:

public string Name = "unnamed";

This line of code states that the Name field, which is a string with the value "unnamed", can be accessed publicly. Besides public, the other two main access modifiers in C# are private and protected, which you will look at them in detail later.

Note

You can find more information regarding access modifiers at https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/access-modifiers.

Everything a class holds is called a class member. Class members can be accessed from outside of a class; however, such access needs to be granted explicitly using the public access modifier. By default, all members have a private access modifier.

You can access class members by writing the object name followed by a dot (.) and the member name. For example, consider the following snippet in which two objects of the Dog class are being created:

Dog sparky = new Dog();

Dog ricky = new Dog();

Here, you can declare two independent variables, sparky and ricky. However, you haven't explicitly assigned these names to the objects; note that these are only the variable names. To assign the names to the objects, you can write the following code using dot notation:

sparky.Name = "Sparky";

ricky.Name = "Ricky";

You can now have hands-on experience of creating classes and objects through an exercise.

Exercise 2.01: Creating Classes and Objects

Consider that there are two books, both by an author named New Writer. The first one, called First Book, was published by Publisher 1. There is no description available for this book. Similarly, the second one is named Second Book and was published by Publisher 2. It has a description that simply says, "Interesting read".

In this exercise, you will model these books in code. The following steps will help you complete this exercise.

  1. Create a class called Book. Add fields for Title, Author, Publisher, Description, and the number of pages. You must print this information from outside the class, so make sure every field is public:

        public class Book

        {

            public string Title;

            public string Author;

            public string Publisher;

            public int Pages;

            public string Description;

        }

  2. Create a class named Solution, with the Main method. As you saw in Chapter 1, Hello C#, this class with the Main method is the starting point of your application:

        public static class Solution

        {

            public static void Main()

            {

            }

        }

  3. Inside the Main method, create an object for the first book and set the values for the fields, as follows:

    Book book1 = new Book();

    book1.Author = "New Writer";

    book1.Title = "First Book";

    book1.Publisher = "Publisher 1";

Here, a new object named book1 is created. Values are assigned to different fields by writing dot (.) followed by the field name. The first book does not have a description, so you can omit the field book1.Description.

  1. Repeat this step for the second book. For this book, you need to set a value for the Description field as well:

    Book book2 = new Book();

    book2.Author = "New Writer";

    book2.Title = "Second Book";

    book2.Publisher = "Publisher 2";

    book2.Description = "Interesting read";

In practice, you will rarely see fields with public access modifiers. Data mutates easily, and you might not want to leave your program open to external changes after initialization.

  1. Inside the Solution class, create a method named Print, which takes a Book object as an argument and prints all fields and their values. Use string interpolation to concatenate book information and print it to the console using Console.WriteLine(), as follows:

    private static void Print(Book book)

    {

        Console.WriteLine($"Author: {book.Author}, " +

                          $"Title: {book.Title}, " +

                          $"Publisher: {book.Publisher}, " +

                          $"Description: {book.Description}.");

    }

  2. Inside the Main method, call the Print method for book1 and book2:

    Print(book1);

    Print(book2);

Upon running this code, you will see the following output on the console:

Author: New Writer, Title: First Book, Publisher: Publisher 1, Description: .

Author: New Writer, Title: Second Book, Publisher: Publisher 2, Description: Interesting read.

Note

You can find the code used for this exercise at https://packt.link/MGT9b.

In this exercise, you saw how to use fields and class members are used in simple programs. Now proceed to know about reference types.

Reference Types

Suppose you have an object and the object is not created, just declared, as follows:

Dog speedy;

What would happen if you tried accessing its Name value? Calling speedy.Name would throw a NullReferenceException exception because speedy is yet to be initialized. Objects are reference types, and their default value is null until initialized. You have already worked with value types, such as int, float, and decimal. Now you need to grasp that there are two major differences between value and reference types.

Firstly, value types allocate memory on the stack, whereas reference types allocate memory on the heap. The stack is a temporary place in memory. As the name implies, in a stack, blocks of memory are stacked on top of each other. When you call a function, all local function variables will end up on a single block of the stack. If you call a nested function, the local variables of that function will be allocated on another block of the stack.

In the following figure, you can see which parts of code will allocate memory in the stack during execution, and which will do so in the heap. Method calls (1, 8, 10) and local variables (2, 4) will be stored in the stack. Objects (3, 5) and their members (6) will be stored on the heap. Stacks use the Push method to allocate data, and Pop to deallocate it. When memory is allocated, it comes on top of the stack. When it is deallocated, it is removed from the top as well. You deallocate memory from the stack as soon as you leave the scope of a method (8, 10, 11). Heap is much more random, and Garbage Collector (GC) automatically (unlike some other languages, where you need to do it yourself), deallocates memory.

Note

GC is a massive topic in itself. If you want to find out more, please refer to the official Microsoft documentation at https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals.

Figure 2.1: Stack and heap comparison

Figure 2.1: Stack and heap comparison

Note

If you make too many nested calls, you will run into a StackoverflowException exception because the stack ran out of memory. Freeing up memory on the stack is just a matter of exiting from a function.

The second difference is that, when value types are passed to a method, their value is copied, while for reference types, only the reference is copied. This means that the reference type object's state is modifiable inside a method, unlike a value type, because a reference is simply the address of an object.

Consider the following snippet. Here, a function named SetTo5 sets the value of the number to 5:

private static void SetTo5(int number)

{

        number = 5;

}

Now, consider the following code:

int a = 2;

// a is 2

Console.WriteLine(a);

SetTo5(a);

// a is still 2

Console.WriteLine(a);

This should result in the following output:

2

2

If you run this code, you find that the printed value of a is still 2 and not 5. This is because a is a value type that passed the value 2, and therefore its value is copied. Inside a function, you never work with the original; a copy is always made.

What about reference types? Suppose you add a field named Owner inside the Dog class:

public class Dog

{ public string Owner;

}

Create a function, ResetOwner, that sets the value of the Owner field for an object to None:

private static void ResetOwner(Dog dog)

{

    dog.Owner = "None";

}

Now, suppose the following code is executed:

Dog dog = new Dog("speedy");

Console.WriteLine(dog.Owner);

ResetOwner(dog);

// Owner is "None"- changes remain

Console.WriteLine(dog.Owner);

This should result in the following output:

speedy

None

Note

You can find the code used for this example at https://packt.link/gj164.

If you try running this snippet of code yourself, you will first see the name speedy on one line and then None printed on another. This would change the dog's name, and the changes would remain outside the function. This is because Dog is a class, and a class is a reference type. When passed to a function, a copy of a reference is made. However, a copy of a reference points to the whole object, and therefore the changes that are made remain outside as well.

It might be confusing to hear that you pass a copy of a reference. How can you be sure you are working with a copy? To learn this, consider the following function:

private static void Recreate(Dog dog)

{

    dog = new Dog("Recreated");

}

Here, creating a new object creates a new reference. If you change the value of a reference type, you are working with a completely different object. It may be one that looks the same but is stored in a completely different place in memory. Creating an object for a passed parameter will not affect anything outside the object. Though this may sound potentially useful, you should generally avoid doing this as it can make code difficult to comprehend.

Properties

The Dog class has one flaw. Logically, you wouldn't want the name of a dog to be changed once it is assigned. However, as of now, there is nothing that prevents changing it. Think about the object from the perspective of what you can do with it. You can set the name of a dog (sparky.Name = "Sparky") or you can get it by calling sparky.Name. However, what you want is a read-only name that can be set just once.

Most languages take care of this through setter and getter methods. If you add the public modifier to a field, this means that it can be both retrieved (read) and modified (written). It isn't possible to allow just one of these actions. However, with setters and getters, you can restrict both read and write access. In OOP, restricting what can be done with an object is key to ensuring data integrity. In C#, instead of setter and getter methods, you can use properties.

In OOP languages (for example Java), to set or get the values of a name, you would write something like this:

public string GetName()

{

    return Name;

}

public string SetName (string name)

{

    Name = name;

}

In C#, it is as simple as the following:

public string Name {get; set;}

This is a property, which is nothing but a method that reads like a field. There are two types of properties: getters and setters. You can perform both read and write operations with them. From the preceding code, if you remove get, it will become write-only, and if you remove set, it will become read-only.

Internally, the property includes a setter and a getter method with a backing field. A backing field is simply a private field that stores a value, and getter and setter methods work with that value. You can write custom getters and setters as well, as follows:

private string _owner;

public string Owner

{

    get

    {

        return _owner;

    }

    set

    {

        _owner = value;

    }

}

In the preceding snippet, the Owner property shows what the default getter and setter methods would look like for the Dog class.

Just like other members, individual parts of a property (either getter or setter) can have their own access modifier, like the following:

public string Name {get; private set;}

In this case, the getter is public, and the setter is private. All parts of the property (getter, setter, or both, as defined) take the access modifier from the property (Name, in this case) unless explicitly specified otherwise (as in the case of private set). If you do not need to set a name, you can get rid of the setter. If you need a default value, you can write the code for this as follows:

public string Name {get;} = "unnamed";

This piece of code means that the Name field is read-only. You can set the name only through a constructor. Note that this is not the same as a private set because the latter means you can still change the name within the Dog class itself. If no setter is provided (as is the case here), you can set the value in only one place, the constructor.

What happens internally when you create a read-only property? The following code is generated by the compiler:

private readonly string _name;

public string get_Name()

{

    return _name;

}

This shows that getter and setter properties are simply methods with a backing field. It is important to note that, if you have a property called Name, the set_Name() and get_Name() methods will be reserved because that's what the compiler generates internally.

You may have noticed a new keyword in the previous snippet, readonly. It signifies that the value of a field can only be initialized once—either during declaration or in a constructor.

Returning a backing field with a property may seem redundant sometimes. For example, consider the next snippet:

private string _name;

public string Name

{

    get

    {

        return "Dog's name is " + _name;

    }

}

This code snippet is a custom property. When a getter or a setter is more than just a basic return, you can write the property in this way to add custom logic to it. This property, without affecting the original name of a dog, will prepend Dog's name is before returning the name. You can make this more concise using expression-bodied property syntax, as follows:

public string Name => "Dog's name is " + _name;

This code does the same thing as the previous code; the => operator indicates that it is a read-only property, and you return a value that is specified on the right side of the => operator.

How do you set the initial value if there is no setter? The answer to that is a constructor. In OOP, a constructor serves one purpose—that is, setting the initial values of fields. Using a constructor is great for preventing the creation of objects in an invalid state.

To add some validation to the Dog class, you can write the following code:

public Dog(string name)

{

  if(string.IsNullOrWhitespace(name))

  {

    throw new ArgumentNullException("name")

  }

  Name = name;

}

The code you have just written will prevent an empty name from being passed when creating a Dog instance.

It is worth mentioning that within a class, you have access to the object itself that will be created. It might sound confusing, but it should make sense with this example:

private readonly string name;

public Dog(string name)

{

  this.name = name;

}

The this keyword is most often used to clear the distinction between class members and arguments. this refers to the object that has just been created, hence, this.name refers to the name of that object and name refers to the passed parameter.

Creating an object of the Dog class, and setting the initial value of a name, can now be simplified as follows:

Dog ricky = new Dog("Ricky");

Dog sparky = new Dog("Sparky");

You still have a private setter, meaning the property that you have is not entirely read-only. You can still change the value of a name within the class itself. However, fixing that is quite easy; you can simply remove the setter and it will become truly read-only.

Note

You can find the code used for this example at http://packt.link/hjHRV.

Object Initialization

Often, a class has read and write properties. Usually, instead of setting the property values via a constructor, they are assigned after the creation of an object. However, in C# there is a better way—object initialization. This is where you create a new object and set the mutable (read and write) field values right away. If you had to create a new object of the Dog class and set the value of Owner for this object to Tobias, you could add the following code:

Dog dog = new Dog("Ricky");

dog.Owner = "Tobias";

This can be done using object initialization as follows:

Dog dog = new Dog("Ricky")

{

  Owner = "Tobias"

};

Setting initial properties like this when they are not a part of a constructor is generally more concise. The same applies to arrays and other collection types. Suppose you had two objects of the Dog class, as follows:

Dog ricky = new Dog("Ricky");

Dog sparky = new Dog("Sparky");

In such a case, one way of creating an array would be as follows:

Dog[] dogs = new Dog[2];

dogs[0] = ricky;

dogs[1] = sparky;

However, instead of this, you can just add the following code, which is more concise:

Dog[] dogs = {ricky, sparky};

In C# 10, you can simplify object initialization without providing the type, if it can be inferred from the declaration, as in the following code:

Dog dog = new("Dog");

Comparing Functions and Methods

Up until now, you might have seen the terms—function and method—used quite often, almost interchangeably. Now proceed to gain further insight into functions and methods. A function is a block of code that you can call using its name and some input. A method is a function that exists within a class.

However, in C#, you cannot have functions outside of a class. Therefore, in C#, every function is a method. Many languages, especially non-OOP languages, have only some functions that can be called methods (for example, JavaScript).

The behavior of a class is defined using methods. You have already defined some behavior for the Dog class, that is, getting its name. To finish implementing the behaviors for this class, you can implement some real-world parallels, such as sitting and barking. Both methods will be called from the outside:

public void Sit()

{

    // Implementation of how a dog sits

}

public void Bark()

{

    // Implementation of how a dog barks

}

You can call both methods like this:

Ricky.Sit();

Sparky.Bark();

In most cases, it is preferable to avoid exposing data publicly, so you should only ever expose functions publicly. Here, you might be wondering, What about properties? Properties are just getter and setter functions; they work with data but aren't data themselves. You should avoid exposing data publicly directly, for the same reason you lock your doors, or carry your phone in a case. If data were public, everyone could access it without any restrictions.

Also, data should not change when the program requires it to be constant. A method is a mechanism that ensures that an object is not used in invalid ways, and if it is, it's well handled.

What if you need to validate the fields consistently throughout the application? Again, properties, that is, getter and setter methods, can help with this. You can limit what you can do with data and add validation logic to it. Properties help you be in full control of how you can get and set data. Properties are handy, but it's important to use them with discretion. If you want to do something complex, something that needs extra computing, it is preferable to use a method.

For example, imagine that you have a class for an inventory made up of items, each having some weight. Here, it might make sense to have a property to return the heaviest item. If you chose to do so through a property (call it MaxWeight), you might get unexpected results; getting the heaviest item would require iterating through a collection of all items and finding the maximum by weight. This process is not as fast as you would expect. In fact, in some cases, it might even throw an error. Properties should have simple logic, otherwise working with them might yield unexpected results. Therefore, when the need for compute-heavy properties arises, consider refactoring them to a method. In this case, you would refactor the MaxWeight property into the GetMaxWeight method.

Properties should be avoided for returning results of complex calculations, as calling a property could be expensive. Getting or setting the value of a field should be straightforward. If it becomes expensive, it should no longer be treated as property.

An Effective Class

The Dog class models a dog object; therefore, it can be called a model. Some developers prefer to have a strict separation between data and logic. Others try to put as much logic in a model as possible, so long as it is self-contained. There is no right or wrong way here. It all depends on the context you are working with.

Note

This discussion is outside the scope of this chapter, but if you would like to know more, you can refer to the discussion on Domain-Driven Design (DDD) at https://martinfowler.com/bliki/DomainDrivenDesign.html.

It is hard to pinpoint what an effective class looks like. However, when deciding whether a method fits better in class A or class B, try asking yourself these questions:

  • Would someone, who is not a programmer, know that you are talking about the class? Is it a logical representation of a real-world concept?
  • How many reasons does the class have to change? Is it just one or are there more reasons?
  • Is private data tightly related to public behavior?
  • How often does the class change?
  • How easy is it to break the code?
  • Does the class do something by itself?

High cohesion is a term used to describe a class that has all its members strongly related, not only semantically, but logically as well. In contrast, a low cohesion class has loosely related methods and fields that probably could have a better place. Such a class is inefficient because it changes for multiple reasons and you cannot expect to look for anything inside it, as it simply has no strong logical meaning.

For example, a part of a Computer class could look like this:

class Computer

{

    private readonly Key[] keys;

}

However, Computer and keys are not related at the same level. There could be another class that better suits the Key class, that is Keyboard:

class Computer

{

    private readonly Keyboard keyboard;

}

class Keyboard

{

    private readonly Key[] keys;

}

Note

You can find the code used for this example at https://packt.link/FFcDa.

A keyboard is directly related to keys, just as it is directly related to a computer. Here, both Keyboard and the Computer class have high cohesion because the dependencies have a stable logical place. You can now learn more about it through an exercise.

Exercise 2.02: Comparing the Area Occupied by Different Shapes

You have two sections of a backyard, one with circular tiles and the other with rectangular tiles. You would like to deconstruct one section of the backyard, but you are not sure which one it should be. Obviously, you want as little mess as possible and have decided to pick the section that occupies the least area.

Given two arrays, one for different sized rectangular tiles and the other for different-sized circular tiles, you need to find which section to deconstruct. This exercise aims to output the name of the section occupying less area, that is, rectangular or  circular.

Perform the following steps to do so:

  1. Create a Rectangle class as follows. It should have fields for width, height, and area:

    public class Rectangle

    {

        private readonly double _width;

        private readonly double _height;

        public double Area

        {

            get

            {

                return _width * _height;

            }

        }

        public Rectangle(double width, double height)

        {

            _width = width;

            _height = height;

        }

    }

Here, _width and _height have been made immutable, using the readonly keyword. The type chosen is double because you will be performing math operations. The only property that is exposed publicly is Area. It will return a simple calculation: the product of width and height. The Rectangle is immutable, so all it needs is to be passed once through a constructor and it remains constant thereafter.

  1. Similarly, create a Circle class as follows:

    public class Circle

    {

        private readonly double _radius;

        public Circle(double radius)

        {

            _radius = radius;

        }

        public double Area

        {

            get { return Math.PI * _radius * _radius; }

        }

    }

The Circle class is similar to Rectangle class, except that instead of width and height, it has radius, and the Area calculation uses a different formula. The constant PI has been used, which can be accessed from the Math namespace.

  1. Create a Solution class with a skeleton method named Solve:

    public static class Solution

    {

        public const string Equal = "equal";

        public const string Rectangular = "rectangular";

        public const string Circular = "circular";

        public static string Solve(Rectangle[] rectangularSection, Circle[] circularSection)

        {

            var totalAreaOfRectangles = CalculateTotalAreaOfRectangles(rectangularSection);

            var totalAreaOfCircles = CalculateTotalAreaOfCircles(circularSection);

            return GetBigger(totalAreaOfRectangles, totalAreaOfCircles);

        }

    }

Here, the Solution class demonstrates how the code works. For now, there are three constants based on the requirements (which section is bigger? rectangular or circular, or are they equal?). Also, the flow will be to calculate the total area of rectangles, then of circles and finally return the bigger.

Before you can implement the solution, you must first create side methods for calculating the total area of the rectangular section, calculating the total area of the circular section, and comparing the two. You will do this over the next few steps.

  1. Inside Solution class, add a method to calculate the total area of the rectangular section:

    private static double CalculateTotalAreaOfRectangles(Rectangle[] rectangularSection)

    {

        double totalAreaOfRectangles = 0;

        foreach (var rectangle in rectangularSection)

        {

            totalAreaOfRectangles += rectangle.Area;

        }

        return totalAreaOfRectangles;

    }

This method goes through all the rectangles, gets the area of each, and adds it to the total sum.

  1. Similarly, add a method to calculate the total area of the circular section:

    private static double CalculateTotalAreaOfCircles(Circle[] circularSection)

    {

        double totalAreaOfCircles = 0;

        foreach (var circle in circularSection)

        {

            totalAreaOfCircles += circle.Area;

        }

        return totalAreaOfCircles;

    }

  2. Next, add a method to get the bigger area, as follows:

    private static string GetBigger(double totalAreaOfRectangles, double totalAreaOfCircles)

    {

        const double margin = 0.01;

        bool areAlmostEqual = Math.Abs(totalAreaOfRectangles - totalAreaOfCircles) <= margin;

        if (areAlmostEqual)

        {

            return Equal;

        }

        else if (totalAreaOfRectangles > totalAreaOfCircles)

        {

            return Rectangular;

        }

        else

        {

            return Circular;

        }

    }

This snippet contains the most interesting part. In most languages, numbers with a decimal point are not accurate. In fact, in most cases, if a and b are floats or doubles, it is likely that they will never be equal. Therefore, when comparing such numbers, you must consider precision.

In this code, you have defined the margin, to have an acceptable range of accuracy of your comparison for when the numbers are considered equal (for example, 0.001 and 0.0011 will be equal in this case since the margin is 0.01). After this, you can do a regular comparison and return the value for whichever section has the biggest area.

  1. Now, create the Main method, as follows:

    public static void Main()

    {

        string compare1 = Solve(new Rectangle[0], new Circle[0]);

        string compare2 = Solve(new[] { new Rectangle(1, 5)}, new Circle[0]);

        string compare3 = Solve(new Rectangle[0], new[] { new Circle(1) });

        string compare4 = Solve(new []

        {

            new Rectangle(5.0, 2.1),

            new Rectangle(3, 3),

        }, new[]

        {

            new Circle(1),

            new Circle(10),

        });

        Console.WriteLine($"compare1 is {compare1}, " +

                          $"compare2 is {compare2}, " +

                          $"compare3 is {compare3}, " +

                          $"compare4 is {compare4}.");

    }

Here, four sets of shapes are created for comparison. compare1 has two empty sections, meaning they should be equal. compare2 has a rectangle and no circles, so the rectangle is bigger. compare3 has a circle and no rectangle, so the circles are bigger. Finally, compare4 has both rectangles and circles, but the total area of the circles is bigger. You used string interpolation inside Console.WriteLine to print the results.

  1. Run the code. You should see the following being printed to the console:

    compare1 is equal, compare2 is rectangular, compare3 is circular, compare4 is circular.

    Note

    You can find the code used for this exercise at https://packt.link/tfDCw.

What if you did not have objects? What would the section be made of in that case? For a circle, it might be viable to just pass radii, but for rectangles, you would need to pass another collinear array with widths and heights.

Object-oriented code is great for grouping similar data and logic under one shell, that is, a class, and passing those class objects around. In this way, you can simplify complex logic through simple interaction with a class.

You will now know about the four pillars of OOP.

The Four Pillars of OOP

Efficient code should be easy to grasp and maintain, and OOP strives to achieve such simplicity. The entire concept of object-oriented design is based on four main tenets, also known as the four pillars of OOP.

Encapsulation

The first pillar of OOP is encapsulation. It defines the relationship between data and behavior, placed in the same shell, that is, a class. It refers to the need to expose only what is necessary and hide everything else. When you think about encapsulation, think about the importance of security for your code: what if you leak a password, return confidential data, or make an API key public? Being reckless often leads to damage that can be hard to fix.

Security is not just limited to protection from malicious intent, but also extends to preventing manual errors. Humans tend to make mistakes. In fact, the more options there are to choose from, the more mistakes they are likely to make. Encapsulation helps in that regard because you can simply limit the number of options available to the person who will use the code.

You should prevent all access by default, and only grant explicit access when necessary. For example, consider a simplified LoginService class:

public class LoginService

{

    // Could be a dictionary, but we will use a simplified example.

    private string[] _usernames;

    private string[] _passwords;

    public bool Login(string username, string password)

    {

        // Do a password lookup based on username

        bool isLoggedIn = true;

        return isLoggedIn;

    }

}

This class has two private fields: _usernames and _passwords. The key point to note here is that neither passwords nor usernames are accessible to the public, but you can still achieve the required functionality by exposing just enough logic publicly, through the Login method.

Note

You can find this code used for this example at https://packt.link/6SO7a.

Inheritance

A police officer can arrest someone, a mailman delivers mail, and a teacher teaches one or more subjects. Each of them performs widely different duties, but what do they all have in common? In the context of the real world, they are all human. They all have a name, age, height, and weight. If you were to model each, you would need to make three classes. Each of those classes would look the same, other than one unique method for each. How could you express in code that they are all human?

The key to solving this problem is inheritance. It allows you to take all the properties from a parent class and transfer them to its child class. Inheritance also defines an is-a relationship. A police officer, a mailman, and a teacher are all humans, and so you can use inheritance. You will now write this down in code.

  1. Create a Human class that has fields for name, age, weight, and height:

    public class Human

    {

        public string Name { get; }

        public int Age { get; }

        public float Weight { get; }

        public float Height { get; }

        public Human(string name, int age, float weight, float height)

        {

            Name = name;

            Age = age;

            Weight = weight;

            Height = height;

        }

    }

  2. A mailman is a human. Therefore, the Mailman class should have all that a Human class has, but on top of that, it should have the added functionality of being able to deliver mail. Write the code for this as follows:

    public class Mailman : Human

    {

        public Mailman(string name, int age, float weight, float height) : base(name, age, weight, height)

        {

        }

        public void DeliverMail(Mail mail)

        {

           // Delivering Mail...

        }

    }

Now, look closely at the Mailman class. Writing class Mailman : Human means that Mailman inherits from Human. This means that Mailman takes all the properties and methods from Human. You can also see a new keyword, base. This keyword is used to tell which parent constructor is going to be used when creating Mailman; in this case, Human.

  1. Next, create a class named Mail to represent the mail, containing a field for a message being delivered to an address:

    public class Mail

    {

       public string Message { get; }

       public string Address { get; }

       public Mail(string message, string address)

       {

           Message = message;

           Address = address;

       }

    }

Creating a Mailman object is no different than creating an object of a class that does not use inheritance.

  1. Create mailman and mail variables and tell the mailman to deliver the mail as follows:

    var mailman = new Mailman("Thomas", 29, 78.5f, 190.11f);

    var mail = new Mail("Hello", "Somewhere far far way");

    mailman.DeliverMail(mail);

    Note

    You can find the code used for this example at https://packt.link/w1bbf.

In the preceding snippet, you created mailman and mail variables. Then, you told the mailman to deliver the mail.

Generally, a base constructor must be provided when defining a child constructor. The only exception to this rule is when the parent has a parameter-less constructor. If a base constructor takes no arguments, then a child constructor using a base constructor would be redundant and therefore can be ignored. For example, consider the following snippet:

Public class A

{

}

Public class B : A

{

}

A has no custom constructors, so implementing B would not require a custom constructor either.

In C#, only a single class can be inherited; however, you can have a multi-level deep inheritance. For example, you could have a child class for Mailman named RegionalMailman, which would be responsible for a single region. In this way, you could go deeper and have another child class for RegionalMailman, called RegionalBillingMailman, then EuropeanRegionalBillingMailman, and so on.

When using inheritance, it is important to know that even if everything is inherited, not everything is visible. Just like before, public members only will be accessible from a parent class. However, in C#, there is a special modifier, named protected, that works like the private modifier. It allows child classes to access protected members (just like public members) but prevents them from being accessed from the outside of the class (just like private).

Decades ago, inheritance used to be the answer to many problems and the key to code reuse. However, over time, it became apparent that using inheritance comes at a price, which is coupling. When you apply inheritance, you couple a child class with a parent. Deep inheritance stacks class scope all the way from parent to child. The deeper the inheritance, the deeper the scope. Deep inheritance (two or more levels deep) should be avoided for the same reason you avoid global variables—it is hard to know what comes from where and hard to control the state changes. This, in turn, makes the code difficult to maintain.

Nobody wants to write duplicate code, but what is the alternative? The answer to that is composition. Just as a computer is composed of different parts, code should be composed of different parts as well. For example, imagine you are developing a 2D game and it has a Tile object. Some tiles contain a trap, and some tiles move. Using inheritance, you could write the code like this:

class Tile

{

}

class MovingTile : Tile

{

    public void Move() {}

}

class TrapTile : Tile

{

    public void Damage() {}

}

//class MovingTrapTile : ?

This approach works fine until you face more complex requirements. What if there are tiles that could both be a trap and move? Should you inherit from a moving tile and rewrite the TrapTile functionality there? Could you inherit both? As you have seen, you cannot inherit more than one class at a time, therefore, if you were to implement this using inheritance, you would be forced to both complicate the situation, and rewrite some code. Instead, you could think about what different tiles contain. TrapTile has a trap. MovingTile has a motor.

Both represent tiles, but the extra functionality they each have should come from different components, and not child classes. If you wanted to make this a composition-based approach, you would need to refactor quite a bit.

To solve this, keep the Tile class as-is:

class Tile

{

}

Now, add two components—Motor and Trap classes. Such components serve as logic providers. For now, they do nothing:

class Motor

{

    public void Move() { }

}

class Trap

{

    public void Damage() { }

}

Note

You can find the code used for this example at https://packt.link/espfn.

Next, you define a MovingTile class that has a single component, _motor. In composition, components rarely change dynamically. You should not expose class internals, so apply private readonly modifiers. The component itself can have a child class or change, and so should not be created from the constructor. Instead, it should be passed as an argument (see the highlighted code):

class MovingTile : Tile

{

    private readonly Motor _motor;

    public MovingTile(Motor motor)

    {

        _motor = motor;

    }

    public void Move()

    {

        _motor.Move();

    }

}

Note that the Move method now calls _motor.Move(). That is the essence of composition; the class that holds composition often does nothing by itself. It just delegates the calls of logic to its components. In fact, even though this is just an example class, a real class for a game would look quite similar to this.

You will do the same for TrapTile, except that instead of Motor, it will contain a Trap component:

class TrapTile : Tile

{

    private readonly Trap _trap;

    public TrapTile(Trap trap)

    {

        _trap = trap;

    }

    public void Damage()

    {

        _trap.Damage();

    }

}

Finally, it's time to create the MovingTrapTile class. It has two components that provide logic to the Move and Damage methods. Again, the two methods are passed as arguments to a constructor:

class MovingTrapTile : Tile

{

    private readonly Motor _motor;

    private readonly Trap _trap;

    public MovingTrapTile(Motor motor, Trap trap)

    {

        _motor = motor;

        _trap = trap;

    }

    public void Move()

    {

        _motor.Move();

    }

    public void Damage()

    {

        _trap.Damage();

    }

}

Note

You can find the code used for this example at https://packt.link/SX4qG.

It might seem that this class repeats some code from the other class, but the duplication is negligible, and the benefits are well worth it. After all, the biggest chunk of logic comes from the components themselves, and a repeated field or a call is not significant.

You may have noticed that you inherited Tile, despite not extracting it as a component for other classes. This is because Tile is the essence of all the classes that inherit it. No matter what type a tile is, it is still a tile. Inheritance is the second pillar of OOP. It is powerful and useful. However, it can be hard to get inheritance right, because in order to be maintainable, it truly needs to be very clear and logical. When choosing whether you should use inheritance, consider these factors:

  • Not deep (ideally single level).
  • Logical (is-a relation, as you saw in your tiles example).
  • Stable and extremely unlikely for the relationship between classes to change in the future; not going to be modified often.
  • Purely additive (child class should not use parent class members, except for a constructor).

If any one of these rules is broken, it is recommended to use composition instead of inheritance.

Polymorphism

The third pillar of OOP is polymorphism. To grasp this pillar, it is useful to look at the meaning of the word. Poly means many, and morph means form. So, polymorphism is used to describe something that has many forms. Consider the example of a mailman, Thomas. Thomas is both a human and a mailman. Mailman is the specialized form and Human is the generalized form for Thomas. However, you can interact with Thomas through either of the two forms.

If you do not know the jobs for every human, you can use an abstract class.

An abstract class is a synonym for an incomplete class. This means that it cannot be initialized. It also means that some of its methods may not have an implementation if you mark them with the abstract keyword. You can implement this for the Human class as follows:

public abstract class Human

{

    public string Name { get; }

    protected Human(string name)

    {

        Name = name;

    }

    public abstract void Work();

}

You have created an abstract (incomplete) Human class here. The only difference from earlier is that you have applied the abstract keyword to the class and added a new abstract method, public abstract void Work(). You have also changed the constructor to protected so that it is accessible only from a child class. This is because it no longer makes sense to have it public if you cannot create an abstract class; you cannot call a public constructor. Logically, this means that the Human class, by itself, has no meaning, and it only gets meaning after you have implemented the Work method elsewhere (that is, in a child class).

Now, you will update the Mailman class. It does not change much; it just gets an additional method, that is, Work(). To provide an implementation for abstract methods, you must use the override keyword. In general, this keyword is used to change the implementation of an existing method inside a child class. You will explore this in detail later:

public override void Work()

{

    Console.WriteLine("A mailman is delivering mails.");

}

If you were to create a new object for this class and call the Work method, it would print "A mailman is delivering mails." to the console. To get a full picture of polymorphism, you will now create one more class, Teacher:

public class Teacher : Human

{

    public Teacher(string name, int age, float weight, float height) : base(name, age, weight, height)

    {

    }

    public override void Work()

    {

        Console.WriteLine("A teacher is teaching.");

    }

}

This class is almost identical to Mailman; however, a different implementation for the Work method is provided. Thus, you have two classes that do the same thing in two different ways. The act of calling a method of the same name, but getting different behavior, is called polymorphism.

You already know about method overloading (not to be confused with overriding), which is when you have methods with the same names but different inputs. That is called static polymorphism and it happens during compile time. The following is an example of this:

public class Person

{

    public void Say()

    {

        Console.WriteLine("Hello");

    }

    public void Say(string words)

    {

        Console.WriteLine(words);

    }

}

The Person class has two methods with the same name, Say. One takes no arguments and the other takes a string as an argument. Depending on the arguments passed, different implementations of the method will be called. If nothing is passed, "Hello" will be printed. Otherwise, the words you pass will be printed.

In the context of OOP, polymorphism is referred to as dynamic polymorphism, which happens during runtime. For the rest of this chapter, polymorphism should be interpreted as dynamic polymorphism.

What is the Benefit of Polymorphism?

A teacher is a human, and the way a teacher works is by teaching. This is not the same as a mailman, but a teacher also has a name, age, weight, and height, like a mailman. Polymorphism allows you to interact with both in the same way, regardless of their specialized forms. The best way to illustrate this is to store both in an array of humans values and make them work:

Mailman mailman = new Mailman("Thomas", 29, 78.5f, 190.11f);

Teacher teacher = new Teacher("Gareth", 35, 100.5f, 186.49f);

// Specialized types can be stored as their generalized forms.

Human[] humans = {mailman, teacher};

// Interacting with different human types

// as if they were the same type- polymorphism.

foreach (var human in humans)

{

    human.Work();

}

This code results in the following being printed in the console:

A mailman is delivering mails.

A teacher is teaching.

Note

You can find the code used for this example at https://packt.link/ovqru.

This code was polymorphism in action. You treated both Mailman and Teacher as Human and implemented the Work method for both. The result was different behaviors in each case. The important point to note here is that you did not have to care about the exact implementations of Human to implement Work.

How would you implement this without polymorphism? You would need to write if statements based on the exact type of an object to find the behavior it should use:

foreach (var human in humans)

{

    Type humanType = human.GetType();

    if (humanType == typeof(Mailman))

    {

        Console.WriteLine("Mailman is working...");

    }

    else

    {

        Console.WriteLine("Teaching");

    }

}

As you see, this is a lot more complicated and harder to grasp. Keep this example in mind when you get into a situation with many if statements. Polymorphism can remove the burden of all that branching code by simply moving the code for each branch into a child class and simplifying the interactions.

What if you wanted to print some information about a person? Consider the following code:

Human[] humans = {mailman, teacher};

foreach (var human in humans)

{

    Console.WriteLine(human);

}

Running this code would result in the object type names being printed to the console:

Chapter02.Examples.Professions.Mailman

Chapter02.Examples.Professions.Teacher

In C#, everything derives from the System.Object class, so every single type in C# has a method called ToString(). Each type has its own implementation of this method, which is another example of polymorphism, widely used in C#.

Note

ToString() is different from Work() in that it provides a default implementation. You can achieve that using the virtual keyword, which will be covered in detail later in the chapter. From the point of view of a child class, working with the virtual or abstract keyword is the same. If you want to change or provide behavior, you will override the method.

In the following snippet, a Human object is given a custom implementation of the ToString() method:

public override string ToString()

{

    return $"{nameof(Name)}: {Name}," +

           $"{nameof(Age)}: {Age}," +

           $"{nameof(Weight)}: {Weight}," +

           $"{nameof(Height)}: {Height}";

}

Trying to print information about the humans in the same foreach loop would result in the following output:

Name: Thomas,Age: 29,Weight: 78.5,Height: 190.11

Name: Gareth,Age: 35,Weight: 100.5,Height: 186.49

Note

You can find the code used for this example at https://packt.link/EGDkC.

Polymorphism is one of the best ways to use different underlying behaviors when dealing with missing type information.

Abstraction

The last pillar of OOP is abstraction. Some say that there are only three pillars of OOP because abstraction does not really introduce much that is new. Abstraction encourages you to hide implementation details and simplify interactions between objects. Whenever you need the functionality of only a generalized form, you should not depend on its implementation.

Abstraction could be illustrated with an example of how people interact with their computers. What occurs in the internal circuitry when you turn on the computer? Most people would have no clue, and that is fine. You do not need to know about the internal workings if you only need to use some functionality. All you have to know is what you can do, and not how it works. You know you can turn a computer on and off by pressing a button, and all the complex details are hidden away. Abstraction adds little new to the other three pillars because it reflects each of them. Abstraction is similar to encapsulation, as it hides unnecessary details to simplify interaction. It is also similar to polymorphism because it can interact with objects without knowing their exact types. Finally, inheritance is just one of the ways to create abstractions.

You do not need to provide unnecessary details coming through implementation types when creating functions. The following example illustrates this problem. You need to make a progress bar. It should keep track of the current progress and should increment the progress up to a certain point. You could create a basic class with setters and getters, as follows:

public class ProgressBar

{

    public float Current { get; set; }

    public float Max { get; }

    public ProgressBar(float current, float max)

    {

        Max = max;

        Current = current;

    }

}

The following code demonstrates how to initialize a progress bar that starts at 0 progress and goes up to 100. The rest of the code illustrates what happens when you want to set the new progress to 120. Progress cannot be more than Max, hence, if it is more than bar.Max, it should just remain at bar.Max. Otherwise, you can update the new progress with the value you set. Finally, you need to check whether the progress is complete (at Max value). To do so, you will compare the delta with the allowed margin of error tolerance (0.0001). A progress bar is complete if it is close to tolerance. So, updating progress could look like the following:

var bar = new ProgressBar(0, 100);

var newProgress = 120;

if (newProgress > bar.Max)

{

    bar.Current = bar.Max;

}

else

{

    bar.Current = newProgress;

}

const double tolerance = 0.0001;

var isComplete = Math.Abs(bar.Max - bar.Current) < tolerance;

This code does what is asked for, but it needs a lot of detail for a function. Imagine if you had to use this in other code; you would need to perform the same checks once again. In other words, it was easy to implement but complex to consume. You have so little within the class itself. A strong indicator of that is that you keep on calling the object, instead of doing something inside the class itself. Publicly, it's possible to break the object state by forgetting to check the Max value of progress and setting it to some high or negative value. The code that you wrote has low cohesion because to change ProgressBar, you would do it not within the class but somewhere outside of it. You need to create a better abstraction.

Consider the following snippet:

public class ProgressBar

{

    private const float Tolerance = 0.001f;

    private float _current;

    public float Current

    {

        get => _current;

        set

        {

            if (value >= Max)

            {

                _current = Max;

            }

            else if (value < 0)

            {

                _current = 0;

            }

            else

            {

                _current = value;

            }

        }

    }

With this code, you have hidden the nitty-gritty details. When it comes to updating progress and defining what the tolerance is, that is up to the ProgressBar class to decide. In the refactored code, you have a property, Current, with a backing field, _current, to store the progress. The property setter checks whether progress is more than the maximum and, if it is, it will not allow the value of _current to be set to a higher value, =. It also cannot be negative, as in those cases, the value will be adjusted to 0. Lastly, if it is not negative and not more than the maximum, then you can set _current to whatever value you pass.

Clearly, this code makes it much simpler to interact with the ProgressBar class:

var bar = new ProgressBar(0, 100);

bar.Current = 120;

bool isComplete = bar.IsComplete;

You cannot break anything; you do not have any extra choices and all you can do is defined through minimalistic methods. When you are asked to implement a feature, it is not recommended to do more than what is asked. Try to be minimalistic and simplistic because that is key to an effective code.

Remember that well-abstracted code is full of empathy toward the reader. Just because today, it is easy to implement a class or a function, you should not forget about tomorrow. The requirements change, the implementation changes, but the structure should remain stable, otherwise, your code can break easily.

Note

You can find the code used for this example can be found at https://packt.link/U126i. The code given in GitHub is split into two contrasting examples—ProgressBarGood and ProgressBarBad. Both codes are simple ProgressBar but were named distinctly to avoid ambiguity.

Interfaces

Earlier, it was mentioned that inheritance is not the proper way of designing code. However, you want to have an efficient abstraction as well as support for polymorphism, and little to no coupling. What if you wanted to have robot or ant workers? They do not have a name. Information such as height and weight are irrelevant. And inheriting from the Human class would make little sense. Using an interface solves this conundrum.

In C#, by convention, interfaces are named starting with the letter I, followed by their actual name. An interface is a contract that states what a class can do. It does not have any implementation. It only defines behavior for every class that implements it. You will now refactor the human example using an interface.

What can an object of the Human class do? It can work. Who or what can do work? A worker. Now, consider the following snippet:

public interface IWorker

{

    void Work();

}

Note

Interface methods will never have an access modifier. This is due to the nature of an interface. All the methods that an interface has are methods you would like to access publicly so that you can implement them. The access modifier that the Work method will have is the same as the interface access modifier, in this case, public.

An ant is not a human, but it can work. With an interface, abstracting an ant as a worker is straightforward:

public class Ant : IWorker

{

    public void Work()

    {

        Console.WriteLine("Ant is working hard.");

    }

}

Similarly, a robot is not a human, but it can work as well:

public class Robot : IWorker

{

    public void Work()

    {

        Console.WriteLine("Beep boop- I am working.");

    }

}

If you refer to the Human class, you can change its definition to public abstract class Human : IWorker. This can be read as: Human class implements the IWorker interface.

In the next snippet, Mailman inherits the Human class, which implements the IWorker interface:

public class Mailman : Human

{

    public Mailman(string name, int age, float weight, float height) : base(name, age, weight, height)

    {

    }

    public void DeliverMail(Mail mail)

    {

        // Delivering Mail...

    }

    public override void Work()

    {

        Console.WriteLine("Mailman is working...");

    }

}

If a child class inherits a parent class, which implements some interfaces, the child class will also be able to implement the same interfaces by default. However, Human was an abstract class and you had to provide implementation to the abstract void Work method.

If anyone asked what a human, an ant, and a robot have in common, you could say that they can all work. You can simulate this situation as follows:

IWorker human = new Mailman("Thomas", 29, 78.5f, 190.11f);

IWorker ant = new Ant();

IWorker robot = new Robot();

IWorker[] workers = {human, ant, robot};

foreach (var worker in workers)

{

    worker.Work();

}

This prints the following to the console:

Mailman is working...

Ant is working hard.

Beep boop- I am working.

Note

You can find the code used for the example at https://packt.link/FE2ag.

C# does not support multiple inheritance. However, it is possible to implement multiple interfaces. Implementing multiple interfaces does not count as multiple inheritance. For example, to implement a Drone class, you could add an IFlyer interface:

public interface IFlyer

{

    void Fly();

}

Drone is a flying object that can do some work; therefore it can be expressed as follows:

public class Drone : IFlyer, IWorker

{

    public void Fly()

    {

        Console.WriteLine("Flying");

    }

    public void Work()

    {

        Console.WriteLine("Working");

    }

}

Listing multiple interfaces with separating commas means the class implements each of them. You can combine any number of interfaces, but try not to overdo this. Sometimes, a combination of two interfaces makes up a logical abstraction. If every drone can fly and does some work, then you can write that in code, as follows:

public interface IDrone : IWorker, IFlyer

{

}

And the Drone class becomes simplified to public class Drone : IDrone.

It is also possible to mix interfaces with a base class (but no more than one base class). If you want to represent an ant that flies, you can write the following code:

public class FlyingAnt : Ant, IFlyer

{

    public void Fly()

    {

        Console.WriteLine("Flying");

    }

}

An interface is undoubtedly the best abstraction because depending on it does not force you to depend on any implementation details. All that is required is the logical concepts that have been defined. Implementation is prone to change, but the logic behind relations between classes is not.

If an interface defines what a class can do, is it also possible to define a contract for common data? Absolutely. An interface holds behavior, hence it can hold properties as well because they define setter and getter behavior. For example, you should be able to track the drone, and for this, it should be identifiable, that is, it needs to have an ID. This can be coded as follows:

public interface IIdentifiable

{

    long Id { get; }

}

public interface IDrone : IWorker, IFlyer

{

}

In modern software development, there are several complex low-level details that programmers use on a daily basis. However, they often do so without knowing. If you want to create a maintainable code base with lots of logic and easy-to-grasp code, you should follow these principles of abstraction:

  • Keep it simple and small.
  • Do not depend on details.
  • Hide complexity.
  • Expose only what is necessary.

With this exercise, you will grasp how OOP functions.

Exercise 2.03: Covering Floor in the Backyard

A builder is building a mosaic with which he needs to cover an area of x square meters. You have some leftover tiles that are either rectangular or circular. In this exercise, you need to find out whether, if you shatter the tiles to perfectly fill the area they take up, can the tiles fill the mosaic completely.

You will write a program that prints true, if the mosaic can be covered with tiles, or false, if it cannot. Perform the following steps to do so:

  1. Create an interface named IShape, with an Area property:

    public interface IShape

    {

        double Area { get; }

    }

This is a get-only property. Note that a property is a method, so it is okay to have it in an interface.

  1. Create a class called Rectangle, with width and height and a method for calculating area, called Area. Implement an IShape interface for this, as shown in the following code:

    Rectangle.cs

    public class Rectangle : IShape

    {

        private readonly double _width;

        private readonly double _height;

        public double Area

        {

            get

            {

                return _width * _height;

            }

        }

        public Rectangle(double width, double height)

        {

The only thing required is to calculate the area. Hence, only the Area property is public. Your interface needs to implement a getter Area property, achieved by multiplying width and height.

  1. Create a Circle class with a radius and Area calculation, which also implements the IShape interface:

    public class Circle : IShape

    {

        Private readonly double _radius;

        public Circle(double radius)

        {

            _radius = radius;

        }

        public double Area

        {

            get { return Math.PI * _radius * _radius; }

        }

    }

  2. Create a skeleton Solution class with a method named IsEnough, as follows:

    public static class Solution

    {

            public static bool IsEnough(double mosaicArea, IShape[] tiles)

            {

       }

    }

Both the class and the method are just placeholders for the implementation to come. The class is static because it will be used as a demo and it does not need to have a state. The IsEnough method takes the needed mosaicArea, an array of tiles objects, and returns whether the total area occupied by the tiles is enough to cover the mosaic.

  1. Inside the IsEnough method, use a for loop to calculate the totalArea. Then, return whether the total area covers the mosaic area:

                double totalArea = 0;

                foreach (var tile in tiles)

                {

                    totalArea += tile.Area;

                }

                const double tolerance = 0.0001;

                return totalArea - mosaicArea >= -tolerance;

           }

  2. Inside the Solution class, create a demo. Add several sets of different shapes, as follows:

    public static void Main()

    {

        var isEnough1 = IsEnough(0, new IShape[0]);

        var isEnough2 = IsEnough(1, new[] { new Rectangle(1, 1) });

        var isEnough3 = IsEnough(100, new IShape[] { new Circle(5) });

        var isEnough4 = IsEnough(5, new IShape[]

        {

            new Rectangle(1, 1), new Circle(1), new Rectangle(1.4,1)

        });

        Console.WriteLine($"IsEnough1 = {isEnough1}, " +

                          $"IsEnough2 = {isEnough2}, " +

                          $"IsEnough3 = {isEnough3}, " +

                          $"IsEnough4 = {isEnough4}.");

    }

Here, you use four examples. When the area to cover is 0, then no matter what shapes you pass, it will be enough. When the area to cover is 1, a rectangle of area 1x1 will be just enough. When it's 100, a circle of radius 5 is not enough. Finally, for the fourth example, the area occupied by three shapes is added up, that is, a rectangle of area 1x1, a circle of radius 1, and the second rectangle of area 1.4x1. The total area is 5, which is less than the combined area of these three shapes.

  1. Run the demo. You should see the following output on your screen:

    IsEnough1 = True, IsEnough2 = True, IsEnough3 = False, IsEnough4 = False.

    Note

    You can find the code used for this exercise at https://packt.link/EODE6.

This exercise is very similar to Exercise 2.02. However, even though the assignment is more complex, there is less code than in the previous assignment. By using the OOP pillars, you were able to create a simple solution for a complex problem. You were able to create functions that depend on abstraction, rather than making overloads for different types. Thus, OOP is a powerful tool, and this only scratches the surface.

Everyone can write code that works but writing code that lives for decades and is easy to grasp is hard. So, it is imperative to know about the set of best practices in OOP.

SOLID Principles in OOP

SOLID principles are a set of best practices for OOP. SOLID is an acronym for five principles, namely, single responsibility, open-closed, Liskov substitution, interface segregation, and dependency inversion. You will not explore each of these in detail.

Single Responsibility Principle

Functions, classes, projects, and entire systems change over time. Every change is potentially a breaking one, so you should limit the risk of too many things changing at a time. In other words, a part of a code block should have only a single reason to change.

For a function, this means that it should do just one thing and have no side effects. In practice, this means that a function should either change, or get something, but never do both. This also means that functions responsible for high-level things should not be mixed with functions that perform low-level things. Low-level is all about implementing interactions with hardware, and working with primitives. High-level is focused on compositions of software building blocks or services. When talking about high- and low-level functions, it is usually referred to as a chain of dependencies. If function A calls function B, A is considered higher-level than B. A function should not implement multiple things; it should instead call other functions that implement doing one thing. The general guideline for this is that if you think you can split your code into different functions, then in most cases, you should do that.

For classes, it means that you should keep them small and isolated from one another. An example of an efficient class is the File class, which can read and write. If it implemented both reading and writing, it would change for two reasons (reading and writing):

public class File

{

    public string Read(string filePath)

    {

        // implementation how to read file contents

        // complex logic

        return "";

    }

    public void Write(string filePath, string content)

    {

        // implementation how to append content to an existing file

        // complex logic

    }

}

Therefore, to conform to this principle, you can split the reading code into a class called Reader and writing code into a class called Writer, as follows:

public class Reader

{

    public string Read(string filePath)

    {

        // implementation how to read file contents

        // complex logic

        return "";

    }

}

public class Writer

{

    public void Write(string filePath, string content)

    {

        // implementation how to append content to an existing file

        // complex logic

    }

}

Now, instead of implementing reading and writing by itself, the File class will simply be composed of a reader and writer:

public class File

{

    private readonly Reader _reader;

    private readonly Writer _writer;

    public File()

    {

        _reader = new Reader();

        _writer = new Writer();

    }

    public string Read(string filePath) => _reader.Read(filePath);

    public void Write(string filePath, string content) => _writer.Write(filePath, content);

}

Note

You can find the code used for this example at https://packt.link/PBppV.

It might be confusing because what the class does essentially remains the same. However, now, it just consumes a component and is not responsible for implementing it. A high-level class (File) simply adds context to how lower-level classes (Reader, Writer) will be consumed.

For a module (library), it means that you should strive to not introduce dependencies, which would be more than what the consumer would want. For example, if you are using a library for logging, it should not come with some third-party logging provider-specific implementation.

For a subsystem, it means that different systems should be as isolated as possible. If two (lower level) systems need to communicate, they could call one another directly. A consideration (not mandatory) would be to have a third system (higher-level) for coordination. Systems should also be separated through a boundary (such as a contract specifying communication parameters), which hides all the details. If a subsystem is a big library collection, it should have an interface to expose what it can do. If a subsystem is a web service, it should be a collection of endpoints. In any case, a contract of a subsystem should provide only the methods that the client may want.

Sometimes, the principle is overdone and classes are split so much that making a change requires changing multiple places. It does keep true to the principle, as a class will have a single reason to change, but in such a case, multiple classes will change for the same reason. For example, suppose you have two classes: Merchandise and TaxCalculator. The Merchandise class has fields for Name, Price, and Vat:

public class Merchandise

{

    public string Name { get; set; }

    public decimal Price { get; set; }

    // VAT on top in %

    public decimal Vat { get; set; }

}

Next, you will create the TaxCalculator class. vat is measured as a percentage, so the actual price to pay will be vat added to the original price:

public static class TaxCalculator

{

    public static decimal CalculateNextPrice(decimal price, decimal vat)

    {

        return price * (1 + vat / 100);

    }

}

What would change if the functionality of calculating the price moved to the Merchandise class? You would still be able to perform the required operation. There are two key points here:

  • The operation by itself is simple.
  • Also, everything that the tax calculator needs come from the Merchandise class.

If a class can implement the logic by itself, as long as it is self-contained (does not involve extra components), it usually should. Therefore, a proper version of the code would be as follows:

public class Merchandise

{

    public string Name { get; set; }

    public decimal Price { get; set; }

    // VAT on top in %

    public decimal Vat { get; set; }

    public decimal NetPrice => Price * (1 + Vat / 100);

}

This code moves the NetPrice calculation to the Merchandise class and the TaxCalculator class has been removed.

Note

Singe Responsibility Principle (SRP) can be summarized in a couple of words: split it. You can find the code used for this example at https://packt.link/lWxNO.

Open-Closed Principle

As mentioned previously, every change in code is potentially a breaking one. As a way around this, instead of changing existing code, it is often preferable to write new code. Every software entity should have an extension point, through which the changes should be introduced. However, after this change is done, a software entity should not be interfered with. The Open-Closed Principle (OCP) is hard to implement and takes a lot of practice, but the benefits (a minimum number of breaking changes) are well worth it.

If a multiple-step algorithm does not change, but its individual steps can change, you should split it into several functions. A change for an individual step will no longer affect the entire algorithm, but rather just that step. Such minimization of reasons for a single class or a function to change is what OCP is all about.

Note

You can find more information on OCP at https://social.technet.microsoft.com/wiki/contents/articles/18062.open-closed-principle-ocp.aspx.

Another example where you may want to implement this principle is a function working with combinations of specific values in code. This is called hardcoding and is generally deemed an inefficient practice. To make it work with new values, you might be tempted to create a new function, but by simply removing a hardcoded part and exposing it through function parameters, you can make it extensible. However, when you have variables that are known to be fixed and not changing, it is fine to hardcode them, but they should be flagged as constant.

Previously, you created a file class with two dependencies—Reader and Writer. Those dependencies are hardcoded, and leave you with no extension points. Fixing that will involve two things. First, add the virtual modifier for both the Reader and Writer class methods:

public virtual string Read(string filePath)

public virtual void Write(string filePath, string content)

Then, change the constructor of the File class so that it accepts instances of Reader and Writer, instead of hardcoding the dependencies:

public File(Reader reader, Writer writer)

{

    _reader = reader;

    _writer = writer;

}

This code enables you to override the existing reader and writer behavior and replace it with whatever behavior you want, that is, the File class extension point.

OCP can be summarized in a few words as don't change it, extend it.

Liskov Substitution

The Liskov Substitution Principle (LSP) is one of the most straightforward principles out there. It simply means that a child class should support all the public behavior of a parent class. If you have two classes, Car and CarWreck, where one inherits the other, then you have violated the principle:

class Car

{

    public object Body { get; set; }

    public virtual void Move()

    {

        // Moving

    }

}

class CarWreck : Car

{

    public override void Move()

    {

        throw new NotSupportedException("A broken car cannot start.");

    }

}

Note

You can find the code used for this example at https://packt.link/6nD76.

Both Car and CarWreck have a Body object. Car can move, but what about CarWreck? It can only stay in one place. The Move method is virtual because CarWreck intends to override it to mark it as not supported. If a child can no longer support what a parent can do, then it should no longer inherit that parent. In this case, a car wreck is not a car, it's simply a wreck.

How do you conform to this principle? All you have to do is to remove the inheritance relationship and replicate the necessary behavior and structure. In this case, CarWreck still has a Body object, but the Move method is unnecessary:

class CarWreck

{

    public object Body { get; set; }

}

Code changes happen quite often, and you can sometimes inadvertently use the wrong method to achieve your goals. Sometimes, you couple code in such a way that what you thought was flexible code turns out to be a complex mess. Do not use inheritance as a way of doing code reuse. Keep things small and compose them (again) instead of trying to override the existing behavior. Before things can be reusable, they should be usable. Design for simplicity and you will get flexibility for free.

LSP can be summarized in a few words: don't fake it.

Note

You can find more information on LSP at https://www.microsoftpressstore.com/articles/article.aspx?p=2255313.

Interface Segregation

The interface segregation principle is a special case of the OCP but is only applicable to contracts that will be exposed publicly. Remember, every change you make is potentially a breaking change, and this especially matters in making changes to a contract. Breaking changes are inefficient because they will often require effort to adapt to the change from multiple people.

For example, say you have an interface, IMovableDamageable:

interface IMovableDamageable

{

    void Move(Location location);

    float Hp{get;set;}

}

A single interface should represent a single concept. However, in this case, it does two things: move and manage Hp (hit points). By itself, an interface with two methods is not problematic. However, in scenarios of the implementation needing only a part of an interface, you are forced to create a workaround.

For example, score text is indestructible, but you would like it to be animated and to move it across a scene:

class ScoreText : IMovableDamageable

{

    public float Hp

    {

        get => throw new NotSupportedException();

        set => throw new NotSupportedException();

    }

    public void Move(Location location)

    {

        Console.WriteLine($"Moving to {location}");

    }

}

public class Location

{

}

Note

The point here isn't to print the location; just to give an example of where it is used. It's up to location's implementation whether it will be printed or not as such.

Taking another example, you might have a house that does not move but can be destroyed:

class House : IMovableDamageable

{

    public float Hp { get; set; }

    public void Move(Location location)

    {

        throw new NotSupportedException();

    }

}

In both scenarios, you worked around the issue by throwing NotSupportedException. However, another programmer should not be given an option to call code that never works in the first place. In order to fix the problem of representing too many concepts, you should split the IMoveableDamageable interface into IMoveable and IDamageable:

interface IMoveable

{

    void Move(Location location);

}

interface IDamageable

{

    float Hp{get;set;}

}

And the implementations can now get rid of the unnecessary parts:

class House : IDamageable

{

    public float Hp { get; set; }

}

class ScoreText : IMovable

{

    public void Move(Location location)

    {

        Console.WriteLine($"Moving to {location}");

    }

}

The Console.WriteLine, in the preceding code, would display the namespace name with the class name.

Note

Interface segregation can be summarized as don't enforce it. You can find the code used for this example at https://packt.link/32mwP.

Dependency Inversion

Large software systems can consist of millions of classes. Each class is a small dependency, and if unmanaged, the complexity might stack into something impossible to maintain. If one low-level component breaks, it causes a ripple effect, breaking the whole chain of dependencies. The dependency inversion principle states that you should avoid hard dependence on underlying classes.

Dependency injection is the industry-standard way of implementing dependency inversion. Do not mix the two; one is a principle and the other refers to the implementation of this principle.

Note that you can also implement dependency inversion without dependency injection. For example, when declaring a field, instead of writing something like private readonly List<int> _numbers = new List<int>();, it is preferable to write private readonly IList<int> = _numbers, which shifts dependency to abstraction (IList) and not implementation (List).

What is dependency injection? It is the act of passing an implementation and setting it to an abstraction slot. There are three ways to implement this:

  • Constructor injection is achieved by exposing an abstraction through the constructor argument and passing an implementation when creating an object and then assigning it to a field. Use it when you want to consistently use the same dependency in the same object (but not necessarily the same class).
  • Method injection is done by exposing an abstraction through a method argument, and then passing an implementation when calling that method. Use it when, for a single method, a dependency might vary, and you do not plan to store the dependency throughout that object's lifetime.
  • Property injection is implemented by exposing an abstraction through a public property, and then assigning (or not) that property to some exact implementation. Property injection is a rare way of injecting dependencies because it suggests that dependency might even be null or temporary and there are many ways in which it could break.

Given two types, interface IBartender { } and class Bar : Bartender { }, you can illustrate the three ways of dependency injection for a class called Bar.

First, prepare the Bar class for constructor injection:

class Bar

{

    private readonly IBartender _bartender;

    public Bar(IBartender bartender)

    {

        _bartender = bartender;

    }

}

The constructor injection is done as follows:

var bar = new Bar(new Bartender());

This kind of dependency injection is a dominating kind of inheritance, as it enforces stability through immutability. For example, some bars have just one bartender.

Method injection would look like this:

class Bar

{

    public void ServeDrinks(IBartender bartender)

    {

        // serve drinks using bartender

    }

}

The injection itself is as follows:

var bar = new Bar();

bar.ServeDrinks(new Bartender());

Often, this kind of dependency injection is called interface injection because the method often goes under an interface. The interface itself is a great idea, but that does not change the idea behind this kind of dependency injection. Use method injection when you immediately consume a dependency that you set, or when you have a complex way of setting new dependencies dynamically. For example, it makes sense to use different bartenders for serving drinks.

Finally, property injection can be done like this:

class Bar

{

    public IBartender Bartender { get; set; }

}

Bartender is now injected like this:

var bar = new Bar();

bar.Bartender = new Bartender();

For example, a bar might have bartenders changing shifts, but one bartender at a time.

Note

You can find the code used for this example at https://packt.link/JcmAT.

Property injection in other languages might have a different name: setter injection. In practice, components do not change that often, so this kind of dependency injection is the rarest.

For the File class, this should mean that instead of exposing classes (implementation), you should expose abstractions (interfaces). This means that your Reader and Writer classes should implement some contract:

public class Reader : IReader

public class Writer: IWriter

Your file class should expose reader and writer abstractions, instead of implementations, as follows:

private readonly IReader _reader;

private readonly IWriter _writer;

public File(IReader reader, IWriter writer)

{

    _reader = reader;

    _writer = writer;

}

This allows for a choice of the kind of IReader and IWriter you would like to inject. A different reader may read a different file format, or a different writer may output in a different way. You have a choice.

Dependency injection is a powerful tool that is used often, especially in an enterprise setting. It allows you to simplify complex systems by putting an interface in between and having 1:1 dependencies of implementation-abstraction-implementation.

Writing effective code that does not break can be paradoxical. It is the same as buying a tool from a shop; you can't know for sure how long it will last, or how well it will work. Code, just like those tools, might work now but break in the near future, and you will only know that it does not work if and when it breaks.

Observing and waiting, seeing how the code evolves, is the only way to know for sure if you have written an effective code. In small, personal projects, you might not even notice any changes, unless you expose the project to the public or involve other people. To most people, SOLID principles often sound like old, outdated principles, like over-engineering. But they are actually a set of best practices that have withstood the test of time, formulated by top professionals seasoned in enterprise settings. It is impossible to write perfect, SOLID code right away. In fact, in some cases, it is not even necessary (if a project is small and meant to be short-lived, for example). As someone who wants to produce quality software and work as a professional, you should practice it as early on as possible.

How C# Helps with Object-Oriented Design

So far, the principles you have learned are not language-specific. It is time to learn how to use C# for OOP. C# is a great language because it is full of some very useful features. It is not only one of the most productive languages to work with, but it also allows you to write beautiful, hard-to-break code. With a rich selection of keywords and languages features, you can model your classes completely the way you want, making the intentions crystal clear. This section will delve deep into C# features that help with object-oriented design.

Static

Up till now in this book, you have interacted mostly with static code. This refers to code that does not need new classes and objects, and that can be called right away. In C#, the static modifier can be applied in five different scenarios—methods, fields, classes, constructors, and the using statement.

Static methods and fields are the simplest application of the static keyword:

public class DogsGenerator

{

    public static int Counter { get; private set; }

    static DogsGenerator()

    {

        // Counter will be 0 anyways if not explicitly provided,

        // this just illustrates the use of a static constructor.

        Counter = 0;

    }

    public static Dog GenerateDog()

    {

        Counter++;

        return new Dog("Dog" + Counter);

    }

}

Note

You can find the code used for this example at https://packt.link/748m3.

Here, you created a class called DogsGenerator. A static class cannot be initialized manually (using the new keyword). Internally, it is initialized, but only once. Calling the GenerateDog method returns a new Dog object with a counter next to its name, such as Dog1, Dog2, and Dog3. Writing a counter like this allows you to increment it from everywhere as it is public static and has a setter. This can be done by directly accessing the member from a class: DogsGenerator.Counter++ will increment the counter by 1.

Once again, note that this does not require a call through an object because a static class instance is the same for the entire application. However, DogsGenerator is not the best example of a static class. That's because you have just created a global state. Many people would say that static is inefficient and should be avoided because it might create unpredictable results due to being modified and accessed uncontrollably.

A public mutable state means that changes can happen from anywhere in the application. Other than being hard to grasp, such code is also prone to breaking in the context of applications with multiple threads (that is, it is not thread-safe).

Note

You will learn about threading in detail in Chapter 5, Concurrency: Multithreading Parallel and Async Code.

You can reduce the impact of a global state by making it publicly immutable. The benefit of doing so is that now you are in control. Instead of allowing a counter increment to happen from any place inside a program, you will change it within DogsGenerator only. For the counter property, achieving it is as simple as making the setter property private.

There is one valuable use case for the static keyword though, which is with helper functions. Such functions take an input and return the output without modifying any state internally. Moreover, a class that contains such functions is static and has no state. Another good application of the static keyword is creating immutable constants. They are defined with a different keyword (const). The Math library is probably the best example of helper functions and constants. It has constants such as PI and E, static helper methods such as Sqrt and Abs, and so on.

The DogsGenerator class has no members that would be applicable to an object. If all class members are static, then the class should be static as well. Therefore, you should change the class to public static class DateGenerator. Be aware, however, that depending on static is the same as depending on a concrete implementation. Although they are easy to use and straightforward, static dependencies are hard to escape and should only be used for simple code, or code that you are sure will not change and is critical in its implementation details. For that reason, the Math class is a static class as well; it has all the foundations for arithmetic calculations.

The last application of static is using static. Applying the static keyword before a using statement causes all methods and fields to be directly accessible without the need to call a class. For example, consider the following code:

using static Math;

public static class Demo

{

    public static void Run()

    {

   //No need Math.PI

        Console.WriteLine(PI);

    }

}

This is a static import feature in C#. By using static Math, all static members can be accessed directly.

Sealed

Previously, you mentioned that inheritance should be handled with great care because the complexity can quickly grow out of hand. You can carefully consider complexity when you read and write code, but can you prevent complexity by design? C# has a keyword for stopping inheritance called sealed. If it logically makes no sense to inherit a class, then you should mark it with the sealed keyword. Security-related classes should also be sealed because it is critical to keep them simple and non-overridable. Also, if performance is critical, then methods in inherited classes are slower, compared to being directly in a sealed class. This is due to how method lookup works.

Partial

In .NET, it is quite popular to make desktop applications using WinForms. The way WinForms works is that you can design how your application looks, with the help of a designer. Internally, it generates UI code and all you have to do is double-click a component, which will generate event handler code. That is where the partial class comes in. All the boring, autogenerated code will be in one class and the code that you write will be in another. The key point to note is that both classes will have the same name but be in different files.

You can have as many partial classes as you want. However, the recommended number of partial classes is no more than two. The compiler will treat them as one big class, but to the user, they will seem like two separate ones. Generating code generates new class files, which will overwrite the code you write. Use partial when you are dealing with autogenerated code. The biggest mistake that beginners make is using partial to manage big complex classes. If your class is complex, it's best to split it into smaller classes, not just different files.

There is one more use case for partial. Imagine you have a part of code in a class that is only needed in another assembly but is unnecessary in the assembly it is originally defined in. You can have the same class in different assemblies and mark it as partial. That way, a part of a class that is not needed will only be used where it is needed and be hidden where it should not be seen.

Virtual

Abstract methods can be overridden; however, they cannot be implemented. What if you wanted to have a method with a default behavior that could be overridden in the future? You can do this using the virtual keyword, as shown in the following example:

public class Human

{

    public virtual void SayHi()

    {

        Console.WriteLine("Hello!");

    }

}

Here, the Human class has the SayHi method. This method is prefixed with the virtual keyword, which means that it can change behavior in a child class, for example:

public class Frenchman : Human

{

    public override void SayHi()

    {

        Console.WriteLine("Bonjour!");

    }

}

Note

You can find the code used for this example at https://packt.link/ZpHhI.

The Frenchman class inherits the Human class and overrides the SayHi method. Calling SayHi from a Frenchman object will print Bonjour.

One of the things about C# is that its behavior is hard to override. Upon declaring a method, you need to be explicit by telling the compiler that the method can be overridden. Only virtual methods can be overridden. Interface methods are virtual (because they get behavior later), however, you cannot override interface methods from child classes. You can only implement an interface in a parent class.

An abstract method is the last type of virtual method and is the most similar to virtual in that it can be overridden as many times as you need (in child and grandchild classes).

To avoid having fragile, changing, overridable behavior, the best kind of virtual methods are the ones that come from an interface. The abstract and virtual keywords enable changing class behavior in child classes and overriding it, which can become a big issue if uncontrolled. Overriding behavior often causes both inconsistent and unexpected results, so you should be careful before using the virtual keyword.

Internal

public, private, and protected are the three access modifiers that have been mentioned. Many beginners think that the default class modifier is private. However, private means that it cannot be called from outside a class, and in the context of a namespace, this does not make much sense. The default access modifier for a class is internal. This means that the class will only be visible inside the namespace it is defined in. The internal modifier is great for reusing classes across the same assembly, while at the same time hiding them from the outside.

Conditional Operators

A null reference exception is probably the most common error in programming. For example, refer to the following code:

int[] numbers = null;

numbers.length;

This code will throw NullReferenceException because you are interacting with a variable that has a null value. What is the length of a null array? There is no proper answer to this question, so an exception will be thrown here.

The best way to protect against such an error is to avoid working with null values altogether. However, sometimes it is unavoidable. In those cases, there is another technique called defensive programming. Before using a value that might be null, make sure it is not null.

Now recall the example of the Dog class. If you create a new object, the value of Owner could be null. If you were to determine whether the owner's name starts with the letter A, you would need to check first whether the value of Owner is null, as follows:

if (dog.Owner != null)

{

    bool ownerNameStartsWithA = dog.Owner.StartsWith('A');

}

However, in C#, using null-conditional, this code becomes as simple as the following:

dog.Owner?.StartsWith('A');

Null-conditional (?) is an example of conditional operators in C#. It is an operator that implicitly runs an if statement (a specific if statement is based on the operator) and either returns something or continues work. The Owner?.StartsWith('A') part returns true if the condition is satisfied and false if it is either not satisfied or the object is null.

There are more conditional operators in C# that you will learn about.

Ternary Operators

There is hardly any language that does not have if statements. One of the most common kinds of if statement is if-else. For example, if the value of Owner is null for an instance of the Dog class, you can describe the instance simply as {Name}. Otherwise, you can better describe it as {Name}, dog of {Owner}, as shown in the following snippet:

if (dog1.Owner == null)

{

    description = dog1.Name;

}

else

{

    description = $"{dog1.Name}, dog of {dog1.Owner}";

}

C#, like many other languages, simplifies this by using a ternary operator:

description = dog1.Owner == null

    ? dog1.Name

    : $"{dog1.Name}, dog of {dog1.Owner}";

On the left side, you have a condition (true or false), followed by a question mark (?), which returns the value on the right if the condition is true, followed by a colon (:), which returns the value to the left if the condition is false. $ is a string interpolation literal, which allows you to write $"{dog1.Name}, dog of {dog1.Owner}" over dog1.Name + "dog of" + dog1.Owner. You should use it when concatenating text.

Imagine there are two dogs now. You want the first dog to join the second one (that is, be owned by the owner of the second dog), but this can only happen if the second one has an owner to begin with. Normally, you would use the following code:

if (dog1.Owner != null)

{

    dog2.Owner = dog1.Owner;

}

But in C#, you can use the following code:

dog1.Owner = dog1.Owner ?? dog2.Owner;

Here, you have applied the null-coalescing operator (??), which returns the value to the right if it is null and the value on the left if it is not null. However, you can simplify this further:

dog1.Owner ??= dog2.Owner;

This means that if the value that you are trying to assign (on the left) is null, then the output will be the value on the right.

The last use case for the null-coalescing operator is input validation. Suppose there are two classes, ComponentA and ComponentB, and ComponentB must contain an initialized instance of ComponentA. You could write the following code:

public ComponentB(ComponentA componentA)

{

    if (componentA == null)

    {

        throw new ArgumentException(nameof(componentA));

    }

    else

    {

        _componentA = componentA;

    }

}

However, instead of the preceding code, you can simply write the following:

_componentA = componentA ?? throw new ArgumentNullException(nameof(componentA));

This can be read as If there is no componentA, then an exception must be thrown.

Note

You can find the code used for this example at https://packt.link/yHYbh.

In most cases, null operators should replace the standard if null-else statements. However, be careful with the way you use the ternary operator and limit it to simple if-else statements because the code can become unreadable very quickly.

Overloading Operators

It is fascinating how much can be abstracted away in C#. Comparing primitive numbers, multiplying, or dividing them is easy, but when it comes to objects, it is not that simple. What is one person plus another person? What is a bag of apples multiplied by another bag of apples? It is hard to say, but it can make total sense in the context of some domains.

Consider a slightly better example. Suppose you are comparing bank accounts. Finding out who has more money in a bank account is a common use case. Normally, to compare two accounts, you would have to access their members, but C# allows you to overload comparison operators so that you can compare objects. For example, imagine you had a BankAccount class like so:

public class BankAccount

{

    private decimal _balance;

    public BankAccount(decimal balance)

    {

        _balance = balance;

    }

}

Here, the balance amount is private. You do not care about the exact value of balance; all you want is to compare one with another. You could implement a CompareTo method, but instead, you will implement a comparison operator. In the BankAccount class, you will add the following code:

public static bool operator >(BankAccount account1, BankAccount account2)

    => account1?._balance > account2?._balance;

The preceding code is called an operator overload. With a custom operator overload like this, you can return true when a balance is bigger and false otherwise. In C#, operators are public static, followed by a return type. After that, you have the operator keyword followed by the actual operator that is being overloaded. The input depends on the operator being overloaded. In this case, you passed two bank accounts.

If you tried to compile the code as it is, you would get an error that something is missing. It makes sense that the comparison operators have a twin method that does the opposite. Now, add the less operator overload as follows:

public static bool operator <(BankAccount account1, BankAccount account2)

    => account1?._balance < account2?._balance;

The code compiles now. Finally, it would make sense to have an equality comparison. Remember, you will need to add a pair, equal and not equal:

public static bool operator ==(BankAccount account1, BankAccount account2)

    => account1?._balance == account2?._balance;

public static bool operator !=(BankAccount account1, BankAccount account2)

    => !(account1 == account2);

Next, you will create bank accounts to compare. Note that all numbers have an m appended, as this suffix makes those numbers decimal. By default, numbers with a fraction are double, so you need to add m at the end to make them decimal:

var account1 = new BankAccount(-1.01m);

var account2 = new BankAccount(1.01m);

var account3 = new BankAccount(1001.99m);

var account4 = new BankAccount(1001.99m);

Comparing two bank accounts becomes as simple as this now:

Console.WriteLine(account1 == account2);

Console.WriteLine(account1 != account2);

Console.WriteLine(account2 > account1);

Console.WriteLine(account1 < account2);

Console.WriteLine(account3 == account4);

Console.WriteLine(account3 != account4);

Running the code results in the following being printed to the console:

False

True

True

True

True

False

Note

You can find the code used for this example at https://packt.link/5DioJ.

Many (but not all) operators can be overloaded, but just because you can do so does not mean you should. Overloading operators can make sense in some cases, but in other cases, it might be counterintuitive. Again, remember to not abuse C# features and use them when it makes logical sense, and when it makes code easier to read, learn, and maintain.

Nullable Primitive Types

Have you ever wondered what to do when a primitive value is unknown? For example, say a collection of products are announced. Their names, descriptions, and some other parameters are known, but the price is revealed only before the launch. What type should you use for storing the price values?

Nullable primitive types are primitive types that might have some value or no value. In C#, to declare such a type, you have to add ? after a primitive, as shown in the following code:

int? a = null;

Here, you declared a field that may or may not have a value. Specifically, this means that a can be unknown. Do not confuse this with a default value because, by default, the value of int types is 0.

You can assign a value to a nullable field quite simply, as follows:

a = 1;

And to retrieve its value afterward, you can write the code as follows:

int b = a.Value;

Generics

Sometimes, you will come across situations where you do the exact same thing with different types, where the only difference is because of the type. For example, if you had to create a method that prints an int value, you could write the following code:

public static void Print(int element)

{

    Console.WriteLine(element);

}

If you need to print a float, you could add another overload:

public static void Print(float element)

{

    Console.WriteLine(element);

}

Similarly, if you need to print a string, you could add yet another overload:

public static void Print(string element)

{

    Console.WriteLine(element);

}

You did the same thing three times. Surely, there must be a way to reduce code duplication. Remember, in C#, all types derive from an object type, which has the ToString() method, so you can execute the following command:

public static void Print(object element)

{

    Console.WriteLine(element);

}

Even though the last implementation contains the least code, it is actually the least efficient. An object is a reference type, whereas a primitive is a value type. When you take a primitive and assign it to an object, you also create a new reference to it. This is called boxing. It does not come for free, because you move objects from stack to heap. Programmers should be conscious of this fact and avoid it wherever possible.

Earlier in the chapter, you encountered polymorphism—a way of doing different things using the same type. You can do the same things with different types as well and generics are what enable you to do that. In the case of the Print example, a generic method is what you need:

public static void Print<T>(T element)

{

    Console.WriteLine(element);

}

Using diamond brackets (<>), you can specify a type, T, with which this function works. <T> means that it can work with any type.

Now, suppose you want to print all elements of an array. Simply passing a collection to a WriteLine statement would result in printing a reference, instead of all the elements. Normally, you would create a method that prints all the elements passed. With the power of generics, you can have one method that prints an array of any type:

public static void Print<T>(T[] elements)

{

    foreach (var element in elements)

    {

        Console.WriteLine(element);

    }

}

Please note that the generic version is not as efficient as taking an object type, simply because you would still be using a WriteLine overload that takes an object as a parameter. When passing a generic, you cannot tell whether it needs to call an overload with an int, float, or String, or whether there is an exact overload in the first place. If there was no overload that takes an object for WriteLine, you would not be able to call the Print method. For that reason, the most performant code is actually the one with three overloads. It is not terribly important though because that is just one, very specific scenario where boxing happens anyway. There are so many other cases, however, where you can make it not only concise but performant as well.

Sometimes, the answer to choosing a generic or polymorphic function hides in tiny details. If you had to implement a method for comparing two elements and return true if the first one is bigger, you could do that in C# using an IComparable interface:

public static bool IsFirstBigger1(IComparable first, IComparable second)

{

    return first.CompareTo(second) > 0;

}

A generic version of this would look like this:

public static bool IsFirstBigger2<T>(T first, T second)

    where T : IComparable

{

    return first.CompareTo(second) > 0;

}

The new bit here is where T : IComparable. It is a generic constraint. By default, you can pass any type to a generic class or method. Constraints still allow different types to be passed, but they significantly reduce the possible options. A generic constraint allows only the types that conform to the constraint to be passed as a generic type. In this case, you will allow only the types that implement the IComparable interface. Constraints might seem like a limitation on types; however, they expose the behavior of the constrained types that you can use inside a generic method. Having constraints enables you to use the features of those types, so it is very useful. In this case, you do limit yourself to what types can be used, but at the same time, whatever you pass to the generic method will be comparable.

What if instead of returning whether the first element is bigger, you needed to return the first element itself? You could write a non-generic method as follows:

public static IComparable Max1(IComparable first, IComparable second)

{

    return first.CompareTo(second) > 0

        ? first

        : second;

}

And the generic version would look as follows:

public static T Max2<T>(T first, T second)

    where T : IComparable

{

    return first.CompareTo(second) > 0

        ? first

        : second;

}

Also, it is worth comparing how you will get a meaningful output using each version. With a non-generic method, this is what the code would look like:

int max1 = (int)Comparator.Max1(3, -4);

With a generic version, the code would be like this:

int max2 = Comparator.Max2(3, -4);

Note

You can find the code used for this example at https://packt.link/sIdOp.

In this case, the winner is obvious. In the non-generic version, you have to do a cast. Casting in code is frowned upon because if you do get errors, you will get them during runtime and things might change and the cast will fail. Casting is also one extra action, whereas the generic version is far more fluent because it does not have a cast. Use generics when you want to work with types as-is and not through their abstractions. And returning an exact (non-polymorphic) type from a function is one of the best use cases for it.

C# generics will be covered in detail in Chapter 4, Data Structures and LINQ.

Enum

The enum type represents a set of known values. Since it is a type, you can pass it instead of passing a primitive value to methods. enum holds all the possible values, hence it isn't possible to have a value that it would not contain. The following snippet shows a simple example of this:

public enum Gender

{

    Male,

    Female,

    Other

}

Note

You can find the code used for this example at https://packt.link/gP9Li.

You can now get a possible gender value as if it were in a static class by writing Gender.Other. Enums can easily be converted to an integer using casting—(int)Gender.Male will return 0, (int)Gender.Female will return 1, and so on. This is because enum, by default, starts numbering at 0.

Enums do not have any behavior and they are known as constant containers. You should use them when you want to work with constants and prevent invalid values from being passed by design.

Extension Methods

Almost always, you will be working with a part of code that does not belong to you. Sometimes, this might cause inconvenience because you have no access to change it. Is it possible to somehow extend the existing types with the functionality you want? Is it possible to do so without inheriting or creating new component classes?

You can achieve this easily through extension methods. They allow you to add methods on complete types and call them as if those methods were natively there.

What if you wanted to print a string to a console using a Print method, but call it from a string itself? String has no such method, but you can add it using an extension method:

public static class StringExtensions

{

    public static void Print(this string text)

    {

        Console.WriteLine(text);

    }

}

And this allows you to write the following code:

"Hey".Print();

This will print Hey to the console as follows:

Hey

Note

You can find the code used for this example at https://packt.link/JC5cj.

Extension methods are static and must be placed within a static class. If you look at the semantics of the method, you will notice the use of the this keyword. The this keyword should be the first argument in an extension method. After that, the function continues as normal and you can use the argument with the this keyword as if it was just another argument.

Use extension methods to add (extend, but not the same extensions as what happens with inheritance) new behavior to existing types, even if the type would not support having methods otherwise. With extension methods, you can even add methods to enum types, which is not possible otherwise.

Struct

A class is a reference type, but not all objects are reference types (saved on the heap). Some objects can be created on the stack, and such objects are made using structs.

A struct is defined like a class, but it is used for slightly different things. Now, create a struct named Point:

public struct Point

{

    public readonly int X;

    public readonly int Y;

    public Point(int x, int y)

    {

        X = x;

        Y = y;

    }

}

The only real difference here is the struct keyword, which indicates that this object will be saved on the stack. Also, you might have noticed that there is no use of properties. There are many people who would, instead of Point, type x and y. It is not a big deal, but instead of one variable, you would be working with two. This way of working with primitives is called primitive obsession. You should follow the principles of OOP and work with abstractions, well-encapsulated data, as well as behavior to keep things close so that they have high cohesion. When choosing where to place variables, ask yourself this question: can x change independently of y? Do you ever modify a point? Is a point a complete value on its own? The answer to all of this is yes and therefore putting it in a data structure makes sense. But why choose a struct over a class?

Structs are fast because they do not have any allocations on the heap. They are also fast because they are passed by value (therefore, access is direct, not through a reference). Passing them by value copies the values, so even if you could modify a struct, changes would not remain outside of a method. When something is just a simple, small composite value, you should use a struct. Finally, with structs, you get value equality.

Another effective example of a struct is DateTime. DateTime is just a unit of time, containing some information. It also does not change individually and supports methods such as AddDays, TryParse, and Now. Even though it has several different pieces of data, they can be treated as one unit, as they are date- and time-related.

Most structs should be immutable because they are passed by a copy of a value, so changing something inside a method will not keep those changes. You can add a readonly keyword to a struct, making all its fields readonly:

public readonly struct Point

{

    public int X { get; }

    public int Y { get; }

    public Point(int x, int y)

    {

        X = x;

        Y = y;

    }

}

A readonly struct can have either a readonly field or getter properties. This is useful for the future maintainers of your code base as it prevents them from doing things that you did not design for (no mutability). Structs are just tiny grouped bits of data, but they can have behavior as well. It makes sense to have a method to calculate the distance between two points:

public static double DistanceBetween(Point p1, Point p2)

{

    return Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));

}

The preceding code has a little bit of math in it—that is, distance between two points is the square root of points x's and y's squared differences added together.

It also makes sense to calculate the distance between this and other points. You do not need to change anything because you can just reuse the existing code, passing correct arguments:

public double DistanceTo(Point p)

{

    return DistanceBetween(this, p);

}

If you wanted to measure the distance between two points, you could create them like this:

var p1 = new Point(3,1);

var p2 = new Point(3,4);

And use a member function to calculate distance:

var distance1 = p1.DistanceTo(p2);

Or a static function:

var distance2 = Point.DistanceBetween(p1, p2);

The result for each version will be as follows:

– 3.

Note

You can find the code used for this example at https://packt.link/PtQzz.

When you think about a struct, think about it as just a group of primitives. The key point to remember is that all the data members (properties or fields) in a struct must be assigned during object initialization. It needs to be done for the same reason local variables cannot be used without having a value set initially. Structs do not support inheritance; however, they do support implementing an interface.

Structs are actually a great way to have simple business logic. Structs should be kept simple and should not contain other object references within them; they should be primitive-only. However, a class can hold as many struct objects as it needs. Using structs is a great way of escaping the obsessive use of primitives and using simple logic naturally, within a tiny group of data where it belongs—that is, a struct.

Record

A record is a reference type (unlike a struct, more like a class). However, out of the box, it has methods for comparison by value (both using the equals method and the operator). Also, a record has a different default implementation of ToString(), which no longer prints a type, but instead all the properties. This is exactly what is needed in many cases, so it helps a lot. Finally, there is a lot of syntactic sugar around records, which you are about to witness.

You already know how to create custom types in C#. The only difference between different custom types is the keyword used. For record types, such a keyword is record. For example, you will now create a movie record. It has a Title, Director, Producer, Description, and a ReleaseDate:

public record MovieRecordV1

{

    public string Title { get; }

    public string Director { get; }

    public string Producer { get; }

    public string Description { get; set; }

    public DateTime ReleaseDate { get; }

    public MovieRecordV1(string title, string director, string producer, DateTime releaseDate)

    {

        Title = title;

        Director = director;

        Producer = producer;

        ReleaseDate = releaseDate;

    }

}

So far, you should find this very familiar, because the only difference is the keyword. Regardless of such a minor detail, you already reap major benefits.

Note

The intention of having MovieRecordV1 class in chapter, as against MovieClass in GitHub code, was to have a type, similar to a class and then refactor highlighting how record helps.

Create two identical movies:

private static void DemoRecord()

{

    var movie1 = new MovieRecordV1(

        "Star Wars: Episode I – The Phantom Menace",

        "George Lucas",

        "Rick McCallum",

        new DateTime(1999, 5, 15));

    var movie2 = new MovieRecordV1(

        "Star Wars: Episode I – The Phantom Menace",

        "George Lucas",

        "Rick McCallum",

        new DateTime(1999, 5, 15));

}

So far, everything is the same. Try to print a movie to the console:

    Console.WriteLine(movie1);

The output would be as follows:

MovieRecordV1 { Title = Star Wars: Episode I - The Phantom Menace, Director = George Lucas, Producer

= Rick McCallum, Description = , ReleaseDate = 5/15/1999 12:00:00 AM }

Note

You can find the code used for this example at https://packt.link/xylkW.

If you tried doing the same to a class or a struct object, you would only get a type printed. However, for a record, a default behavior is to print all of its properties and their values.

That is not the only benefit of a record. Again, a record has value-equality semantics. Comparing two movie records will compare them by their property values:

    Console.WriteLine(movie1.Equals(movie2));

    Console.WriteLine(movie1 == movie2);

This will print true true.

With the same amount of code, you have managed to get the most functionality by simply changing a data structure to a record. Out of the box, a record provides Equals(), GetHashCode() overrides, == and != overrides, and even a ToString override, which prints the record itself (all the members and their values). The benefits of records do not end there because, using them, you have a way to reduce a lot of boilerplate code. Take full advantage of records and rewrite your movie record:

public record MovieRecord(string Title, string Director, string Producer, string Description, DateTime ReleaseDate);

This is a positional record, meaning all that you pass as parameters will end up in the right read-only data members as if it was a dedicated constructor. If you ran the demo again, you would notice that it no longer compiles. The major difference with this declaration is that, now, changing a description is no longer possible. Making a mutable property is not difficult, you just need to be explicit about it:

public record MovieRecord(string Title, string Director, string Producer, DateTime ReleaseDate)

{

    public string Description { get; set; }

}

You started this paragraph with a discussion on immutability, but why is the primary focus on records? The benefits of records are actually immutability-focused. Using a with expression, you can create a copy of a record object with zero or more properties modified. So, suppose you add this to your demo:

var movie3 = movie2 with { Description = "Records can do that?" };

movie2.Description = "Changing original";

Console.WriteLine(movie3);

The code would result in this:

MovieRecord { Title = Star Wars: Episode I - The Phantom Menace, Director = George Lucas, Producer

= Rick McCallum, ReleaseDate = 5/15/1999 12:00:00 AM, Description = Records can do that? }

As you see, this code copies an object with just one property changed. Before records, you would need a lot of code to ensure all the members are copied, and only then would you set a value. Keep in mind that this creates a shallow copy. A shallow copy is an object with all the references copied. A deep copy is an object with all the reference-type objects recreated. Unfortunately, there is no way of overriding such behavior. Records cannot inherit classes, but they can inherit other records. They can also implement interfaces.

Other than being a reference type, records are more like structs in that they have value equality and syntactic sugar around immutability. They should not be used as a replacement for structs because structs are still preferable for small and simple objects, which have simple logic. Use records when you want immutable objects for data, which could hold other complex objects (if nested objects could have a state that changes, shallow copying might cause unexpected behavior).

Init-Only Setters

With the introduction of records, the previous edition, C# 9, also introduced init-only setter properties. Writing init instead of set can enable object initialization for properties:

public class House

{

    public string Address { get; init; }

    public string Owner { get; init; }

    public DateTime? Built { get; init; }

}

This enables you to create a house with unknown properties:

var house2 = new House();

Or assign them:

var house1 = new House

{

    Address = "Kings street 4",

    Owner = "King",

    Built = DateTime.Now

};

Using init-only setters is especially useful when you want read-only data, which can be known or not, but not in a consistent matter.

Note

You can find the code used for this example at https://packt.link/89J99.

ValueTuple and Deconstruction

You already know that a function can only return one thing. In some cases, you can use the out keyword to return a second thing. For example, converting a string to a number is often done like this:

var text = "123";

var isNumber = int.TryParse(text, out var number);

TryParse returns both the parsed number and whether the text was a number.

However, C# has a better way of returning multiple values. You can achieve this using a data structure called ValueTuple. It is a generic struct that contains from one to six public mutable fields of any (specified) type. It is just a container for holding unrelated values. For example, if you had a dog, a human, and a Bool, you could store all three in a ValueTuple struct:

var values1 = new ValueTuple<Dog, Human, bool>(dog, human, isDogKnown);

You can then access each—that is, dog through values1.Item1, human through values1.Item2, and isDogKnown through values.Item3. Another way of creating a ValueTuple struct is to use brackets. This does exactly the same thing as before, but using the brackets syntax:

var values2 = (dog, human, isDogKnown);

The following syntax proves extremely useful because, with it, you can declare a function that virtually returns multiple things:

public (Dog, Human, bool) GetDogHumanAndBool()

{

    var dog = new Dog("Sparky");

    var human = new Human("Thomas");

    bool isDogKnown = false;

    return (dog, human, isDogKnown);

}

Note

You can find the code used for this example at https://packt.link/OTFpm.

You can also do the opposite, using another C# feature called deconstruction. It takes object data members and allows you to split them apart, into separate variables. The problem with a tuple type is that it does not have a strong name. As mentioned before, every field will be called ItemX, where X is the order in which the item was returned. Working with all that, GetDogHumanAndBool would require the results to be assigned to three different variables:

var dogHumanAndBool = GetDogHumanAndBool();

var dog = dogHumanAndBool.Item1;

var human = dogHumanAndBool.Item2;

var boo = dogHumanAndBool.Item3;

You can simplify this and instead make use of deconstruction—assigning object properties to different variables right away:

var (dog, human, boo) = GetDogHumanAndBool();

Using deconstruction, you are able to make this a lot more readable and concise. Use ValueTuple when you have multiple unrelated variables and you want to return them all from a function. You do not have to always work around using the out keyword, nor do you have to add overhead by creating a new class. You can solve this problem by simply returning and then deconstructing a ValueTuple struct.

You can now have hands-on experience of using SOLID principles for writing codes incrementally through the following exercise.

Exercise 2.04: Creating a Composable Temperature Unit Converter

Temperature can be measured in different units: Celsius, Kelvin, and Fahrenheit. In the future, more units might be added. However, units do not have to be added dynamically by the user; the application either supports it or not. You need to make an application that converts temperature from any unit to another unit.

It is important to note that converting to and from that unit will be a completely different thing. Therefore, you will need two methods for every converter. As a standard unit, you will use Celsius. Therefore, every converter should have a conversion method from and to Celsius, which makes it the simplest unit of a program. When you need to convert non-Celsius to Celsius, you will need to involve two converters—one to adapt the input to the standard unit (C), and then another one to convert from C to whatever unit you want. The exercise will aid you in developing an application using the SOLID principles and C# features you have learned in this chapter, such as record and enum.

Perform the following steps to do so:

  1. Create a TemperatureUnit that uses an enum type to define constants—that is, a set of known values. You do not need to add it dynamically:

    public enum TemperatureUnit

    {

        C,

        F,

        K

    }

In this example, you will use three temperature units that are C, K, and F.

  1. Temperature should be thought of as a simple object made of two properties: Unit and Degrees. You could either use a record or a struct because it is a very simple object with data. The best choice would be picking a struct here (due to the size of the object), but for the sake of practicing, you will use a record:

    public record Temperature(double Degrees, TemperatureUnit Unit);

  2. Next, add a contract defining what you want from an individual specific temperature converter:

    public interface ITemperatureConverter

    {

        public TemperatureUnit Unit { get; }

        public Temperature ToC(Temperature temperature);

        public Temperature FromC(Temperature temperature);

    }

You defined an interface with three methods—the Unit property to identify which temperature the converter is for, and ToC and FromC to convert from and to standard units.

  1. Now that you have a converter, add the composable converter, which has an array of converters:

    public class ComposableTemperatureConverter

    {

        private readonly ITemperatureConverter[] _converters;

  2. It makes no sense to have duplicate temperature unit converters. So, add an error that will be thrown when a duplicate converter is detected. Also, not having any converters makes no sense. Therefore, there should be some code for validating against null or empty converters:

    public class InvalidTemperatureConverterException : Exception

    {

        public InvalidTemperatureConverterException(TemperatureUnit unit) : base($"Duplicate converter for {unit}.")

        {

        }

        public InvalidTemperatureConverterException(string message) : base(message)

        {

        }

    }

When creating custom exceptions, you should provide as much information as possible about the context of an error. In this case, pass the unit for which the converter was not found.

  1. Add a method that requires non-empty converters:

    private static void RequireNotEmpty(ITemperatureConverter[] converters)

    {

        if (converters?.Length > 0 == false)

        {

            throw new InvalidTemperatureConverterException("At least one temperature conversion must be supported");

        }

    }

Passing an array of empty converters throws an InvalidTemperatureConverterException exception.

  1. Add a method that requires non-duplicate converters:

    private static void RequireNoDuplicate(ITemperatureConverter[] converters)

    {

        for (var index1 = 0; index1 < converters.Length - 1; index1++)

        {

            var first = converters[index1];

            for (int index2 = index1 + 1; index2 < converters.Length; index2++)

            {

                var second = converters[index2];

                if (first.Unit == second.Unit)

                {

                    throw new InvalidTemperatureConverterException(first.Unit);

                }

            }

        }

    }

This method goes through every converter and checks that, at other indexes, the same converter is not repeated (by duplicating TemperatureUnit). If it finds a duplicate unit, it will throw an exception. If it does not, it will just terminate successfully.

  1. Now combine it all in a constructor:

    public ComposableTemperatureConverter(ITemperatureConverter[] converters)

    {

        RequireNotEmpty(converters);

        RequireNoDuplicate(converters);

        _converters = converters;

    }

When creating the converter, validate against converters that are not empty and not duplicates and only then set them.

  1. Next, create a private helper method to help you find the requisite converter, FindConverter, inside the composable converter:

    private ITemperatureConverter FindConverter(TemperatureUnit unit)

    {

        foreach (var converter in _converters)

        {

            if (converter.Unit == unit)

            {

                return converter;

            }

        }

        throw new InvalidTemperatureConversionException(unit);

    }

This method returns the converter of the requisite unit and, if no converter is found, throws an exception.

  1. To simplify how you search and convert from any unit to Celsius, add a ToCelsius method for that:

    private Temperature ToCelsius(Temperature temperatureFrom)

    {

        var converterFrom = FindConverter(temperatureFrom.Unit);

        return converterFrom.ToC(temperatureFrom);

    }

Here, you find the requisite converter and convert the Temperature to Celsius.

  1. Do the same for converting from Celsius to any other unit:

    private Temperature CelsiusToOther(Temperature celsius, TemperatureUnit unitTo)

    {

        var converterTo = FindConverter(unitTo);

        return converterTo.FromC(celsius);

    }

  2. Wrap it all up by implementing this algorithm, standardize the temperature (convert to Celsius), and then convert to any other temperature:

    public Temperature Convert(Temperature temperatureFrom, TemperatureUnit unitTo)

    {

        var celsius = ToCelsius(temperatureFrom);

        return CelsiusToOther(celsius, unitTo);

    }

  3. Add a few converters. Start with the Kelvin converter, KelvinConverter:

    public class KelvinConverter : ITemperatureConverter

    {

        public const double AbsoluteZero = -273.15;

        public TemperatureUnit Unit => TemperatureUnit.K;

        public Temperature ToC(Temperature temperature)

        {

            return new(temperature.Degrees + AbsoluteZero, TemperatureUnit.C);

        }

        public Temperature FromC(Temperature temperature)

        {

            return new(temperature.Degrees - AbsoluteZero, Unit);

        }

    }

The implementation of this and all the other converters is straightforward. All you had to do was implement the formula to convert to the correct unit from or to Celsius. Kelvin has a useful constant, absolute zero, so instead of having a magic number, –273.15, you used a named constant. Also, it is worth remembering that a temperature is not a primitive. It is both a degree value and a unit. So, when converting, you need to pass both. ToC will always take TemperatureUnit.C as a unit and FromC will take whatever unit the converter is identified as, in this case, TemperatureUnit.K.

  1. Now add a Fahrenheit converter, FahrenheitConverter:

    public class FahrenheitConverter : ITemperatureConverter

    {

        public TemperatureUnit Unit => TemperatureUnit.F;

        public Temperature ToC(Temperature temperature)

        {

            return new(5.0/9 * (temperature.Degrees - 32), TemperatureUnit.C);

        }

        public Temperature FromC(Temperature temperature)

        {

            return new(9.0 / 5 * temperature.Degrees + 32, Unit);

        }

    }

Fahrenheit is identical structure-wise; the only differences are the formulas and unit value.

  1. Add a CelsiusConverter, which will accept a value for the temperature and return the same value, as follows:

        public class CelsiusConverter : ITemperatureConverter

        {

            public TemperatureUnit Unit => TemperatureUnit.C;

            public Temperature ToC(Temperature temperature)

            {

                return temperature;

            }

            public Temperature FromC(Temperature temperature)

            {

                return temperature;

            }

        }

CelsiusConverter is the simplest one. It does not do anything; it just returns the same temperature. The converters convert to standard temperature—Celsius to Celsius is always Celsius. Why do you need such a class at all? Without it, you would need to change the flow a bit, adding if statements to ignore the temperature if it was in Celsius. But with this implementation, you can incorporate it in the same flow and use it in the same way with the help of the same abstraction, ITemperatureConverter.

  1. Finally, create a demo:

    Solution.cs

    public static class Solution

    {

        public static void Main()

        {

            ITemperatureConverter[] converters = {new FahrenheitConverter(), new KelvinConverter(), new CelsiusConverter()};

            var composableConverter = new ComposableTemperatureConverter(converters);

            var celsius = new Temperature(20.00001, TemperatureUnit.C);

            var celsius1 = composableConverter.Convert(celsius, TemperatureUnit.C);

            var fahrenheit = composableConverter.Convert(celsius1, TemperatureUnit.F);

            var kelvin = composableConverter.Convert(fahrenheit, TemperatureUnit.K);

            var celsiusBack = composableConverter.Convert(kelvin, TemperatureUnit.C);

            Console.WriteLine($"{celsius} = {fahrenheit}");

In this example, you have created all the converters and passed them to the converters container called composableConverter. Then you have created a temperature in Celsius and used it to perform conversions from and to all the other temperatures.

  1. Run the code and you will get the following results:

    Temperature { Degrees = 20.00001, Unit = C } = Temperature { Degrees = 68.000018, Unit = F }

    Temperature { Degrees = 68.000018, Unit = F } = Temperature { Degrees = -253.14998999999997, Unit = K }

    Temperature { Degrees = -253.14998999999997, Unit = K } = Temperature { Degrees = 20.000010000000003, Unit = C }

    Note

    You can find the code used for this exercise at https://packt.link/dDRU6.

A software developer, ideally, should design code in such a way that making a change now or in the future will take the same amount of time. Using SOLID principles, you can write code incrementally and minimize the risk of breaking changes, because you never change existing code; you just add new code. As systems grow, complexity increases, and it might be difficult to learn how things work. Through well-defined contracts, SOLID enables you to have easy-to-read, and maintainable code because each piece is straightforward by itself, and they are isolated from one another.

You will now test your knowledge of creating classes and overriding operators through an activity.

Activity 2.01: Merging Two Circles

In this activity, you will create classes and override operators to solve the following mathematics problem: A portion of pizza dough can be used to create two circular pizza bites each with a radius of three centimeters. What would be the radius of a single pizza bite made from the same amount of dough? You can assume that all the pizza bites are the same thickness. The following steps will help you complete this activity:

  1. Create a Circle struct with a radius. It should be a struct because it is a simple data object, which has a tiny bit of logic, calculating area.
  2. Add a property to get the area of a circle (try to use an expression-bodied member). Remember, the formula of a circle's area is pi*r*r. To use the PI constant, you will need to import the Math package.
  3. Add two circles' areas together. The most natural way would be to use an overload for a plus (+) operator. Implement a + operator overload that takes two circles and returns a new one. The area of the new circle is the sum of the areas of the two old circles. However, do not create a new circle by passing the area. You need a Radius. You can calculate this by dividing the new area by PI and then taking the square root of the result.
  4. Now create a Solution class that takes two circles and returns a result—the radius of the new circle.
  5. Within the main method, create two circles with a radius of 3 cm and define a new circle, which is equal to the areas of the two other circles added together. Print the results.
  6. Run the main method and the result should be as follows:

    Adding circles of radius of 3 and 3 results in a new circle with a radius 4.242640687119285

As you can see from this final output, the new circle will have a radius of 4.24 (rounded to the second decimal place).

Note

The solution to this activity can be found at https://packt.link/qclbF.

This activity was designed to test your knowledge of creating classes and overriding operators. Operators are not normally employed to solve this sort of problem, but in this case, it worked well.

Summary

In this chapter, you learned about OOP and how it helps take complex problems and abstract them into simple concepts. C# has several useful features and, roughly every one or two years, a new language version is released. The features mentioned in this chapter are just some of the ways in which C# aids in productivity. You have seen how, by design, it allows for better, clearer code, less prone to error. C# is one of the best languages when it comes to productivity. With C#, you can make effective code, and quickly, because a lot of the boilerplate code is done for you.

Finally, you learned the SOLID principles and used them in an application. SOLID is not something you can just read and learn immediately; it takes practice, discussions with your peers, and a lot of trial and error before you get it right and start applying it consistently. However, the benefits are worth it. In modern software development, producing fast, optimal code is no longer a number one priority. Nowadays, the focus is a balance of productivity (how fast you develop) and performance (how fast your program is). C# is one of the most efficient languages out there, both in terms of performance and productivity.

In the next chapter, you will learn what functional programming is and how to work with lambdas and functional constructs such as delegates.

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

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