Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
The C# Workshop

You're reading from   The C# Workshop Kickstart your career as a software developer with C#

Arrow left icon
Product type Paperback
Published in Sep 2022
Publisher Packt
ISBN-13 9781800566491
Length 780 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Almantas Karpavicius Almantas Karpavicius
Author Profile Icon Almantas Karpavicius
Almantas Karpavicius
Jason Hales Jason Hales
Author Profile Icon Jason Hales
Jason Hales
Mateus Viegas Mateus Viegas
Author Profile Icon Mateus Viegas
Mateus Viegas
Arrow right icon
View More author details
Toc

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.

You have been reading a chapter from
The C# Workshop
Published in: Sep 2022
Publisher: Packt
ISBN-13: 9781800566491
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image