Skip to content
AlmantasK edited this page May 9, 2021 · 10 revisions

Introduction

Imagine if I asked you about a car: what is a it made of, what can you do with it? You would start by saying that every car has a body, engine. It can move, has a varying speed, position... What if I asked you to create such a Car? How would you design it?

Car Blueprint

Object-Oriented Programming (OOP) is a programming paradigm, which allows us to model real-world concepts (like the car above) into code.

Class

Class is a blueprint. It defines a design of an object: what it has and what it can do.

Here is a class Car:

public class Car
{
    // What does it have?
    // What can we do with it?
}

What Does it Have?

The first big part of a class are fields. A field is variable inside a class. We use fields to define data of a class.

Let's model the answer of the first question: What does a car have? It has a location, current and maximum speed.

public class Car
{
    // What does a car have?
    // X, Y and Z are parts of a 3D point
    double X;
    double Y;
    double Z;
    float CurrentSpeed;
    float MaxSpeed;
}

What Can We Do With It?

The second big part of a class is methods. A method is a function inside a class. We use methods to define class behavior.

Let's model the answer to the second question: What can we do with a car? We can move it.

public class Car
{
    // What does a car have?
    // ...

    // What can we do with a car?
    void MoveTo(double x, double y, double z)
    {
        // In reality we should also increment current speed and involve time.
        // For simplicity sake- we won't.
        X = x; 
        Y = y; 
        Z = z;
    }
}

Object

Most classes we wrote before were static. We could call functions directly from a static class and even if there was a state, it was shared (global). What if we wanted to have the same class, but with multiple different states? How to create a local scope for a class state?

Object- is an instance of a class. It's the outcome of a specific state applied against a class. Multiple objects can coexist.

New Object

We create an object using a new keyword. We can treat an object just like a variable.

--

We want to be heroes- let's pretend we're Batman/Batwoman and create ourselves a Batmobile!

var batmobile = new Car();

What state does the batmobile have? new Car() sets default values to fields of a car. Therefore the location (x,y,z), max and current speed will all be 0.

Class Members

Methods and fields are class members. We can call a class member using a dot(.). For example, let's try to make the batmobile move:

batmobile.MoveTo(1,1,0);

And... it doesn't work? Trying to call MoveTo will result in an error message: 'Car.MoveTo(double, double, double)' is inaccessible due to its protection level. Protection what?

Access Modifiers

Class members have an access modifier- which tells whether they can be accessed from the outside or not.

There are many access modifiers, but for now it's enough to know 2. Both answer the question: Can a class member be accessed from outside of it:

  • public- yes
  • private- no

If no modifier is specified, a member will be private. That's why we got that error.

Picking The Right Access Modifiers

How to decide, what access modifier is the most appropriate? You should strive to expose only the minimum needed for others to interact with a class. If a class member doesn't have to be public, keep it private.

It's best practice to specify an access modifier for private members as well( regardless of it working the same way without one). Let's fix the Car class:

public class Car
{
    // What does a car have?

    // X, Y and Z are parts of a 3D point
    public double X;
    public double Y;
    public double Z;

    public string Body;
    public float CurrentSpeed;
    
    private float MaxSpeed;

    // What can we do with it?
    public void MoveTo(double x, double y, double z)
    {
        //...
    }
}

Calling MoveTo now works.

We chose X, Y, Z to be public, because it is important to be able to locate a car. Body is public as well, because car looks are important to the public. Is a car speeding? We won't know that unless we expose CurrentSpeed as public.

MaxSpeed was the only thing private, because it is just an internal constraint, which doesn't give much value for public use.

Please note: how we design our car is up to us and in some cases MaxSpeed might be variable or needed for public use. It's all about the subject area.

Constructor

A constructor (ctor in short) is a special method used to create new objects and setting their initial state. Every class must have at least one ctor.

In our case, we did not write our own ctor, yet we were still able to initialize an instance of a class. How? When a class doesn't have any ctor, it will be assigned a default ctor- empty, parameterless method which assigns default values and does nothing else. As soon as we write our own ctor, the default one is replaced. A class can have multiple ctors.

A constructor has the same syntax as a method, except it does not need a return type (it returns the new object after all).

Car with a custom constructor which does nothing (just sets default values):

class Car
{
    public Car()
    {

    }
}

Is equivalent to this:

class Car
{
}

What if you made a constructor private? You guessed it- you wouldn't be able to create new objects of that class.

Prevent invalid state as soon as you can. The soonest you can do it is inside a ctor. Constructor is the best way to specify the bare minimums of a valid object.

A car must start with some body and it's specification must include maximum speed. Maximum speed must be more than 0:

public class Car
{
    // What does a car have?
    // Fields...

    // A constructor is usually: 
    // - below fields
    // - above methods
    public Car(string body, float maximumSpeed)
    {
        ValidateMaximumSpeed(maximumSpeed);

        Body = body;
        MaximumSpeed = maximumSpeed;
    }

    // What can we do with it?
    // Methods...
    private void ValidateMaximumSpeed(float maximumSpeed)
    {
        if(maximumSpeed <= 0)
        {
            throw new Exception($"Maximum speed must be greater than 0, but was {maximumSpeed}");
        }
    }

}

This enables us to:

var batmobile = new Car("Black Batmobile", 400);

Batmobile

The 4 Pillars of OOP

All that you learned so far is the 20% you need to know for 80% of cases. However, just because you are using classes and objects- doesn't mean that you are following the OOP paradigm. In the next part we will discuss more features of OOP and the pillars on top of which it stands and the problems they solve.

The 4 Pillars are: Encapsulation, Inheritance, Polymorphism and Abstraction.

Encapsulation

Encapsulation is the first pillar of OOP. It says 2 things:

  • Group related data and behavior under the same class
  • Hide what you don't need

The benefits of grouping related data and behavior are obvious- that way we can organize code nicely, it becomes intuitive to use and is a good representation of real world concepts.

Hiding members is not so intuitive though. What could go wrong if we made everything public? Having every member public would risk using the object not as intented. For you using your own code- it might be not a problem, but for another person using your code- they are free to mess with it however they want. Follow a rule of thumb- if something can break- it will break. Every public access point is a way of misusing an object and breaking things in between. Hiding members doesn't remove the risk of breaking stuff, but it greately reduces it.

The key point in encapsulation comes down to 2 points:

  • Data is fragile, it can be invalid very fast.
  • Control data through getter and setter methods.

Never expose fields directly. Expose them through getter and setter methods.

Getters and Setters

Getter- is a method to get a value of a field.
Setter- is a method to set a value of a field.

Let's prevent direct access to fields through setter and getter methods on a Car:

public class Car
{
    private double X;
    private double Y;
    private double Z;
    private string Body;
    private float CurrentSpeed;
    private float MaxSpeed;

    // Ctors

    // Methods
    public double GetX()
    {
        return X;
    }

    public double GetY()
    {
        return Y;
    }

    public double GetZ()
    {
        return Z;
    }

    public string GetBody()
    {
        return Body;
    }

    public float SetCurrentSpeed(float speed)
    {
        CurrentSpeed = speed;
    }

    public float GetCurrentSpeed()
    {
        return speed;
    }
}

The whole point of this code is that it prevents doing things that we're not supposed to do with a Car. What aren't we supposed to do? We should not change the location of a car one part of a point at the time- therefore there is no setter for individual parts (x,y,z), but only a method MoveTo(x,y,z) which assigns the location. Changing a body of a car would mean it needs a complete rework- it almost means it's a new car- therefore only a getter method is needed. Max speed doesn't have any getter/setter methods- we said it's used only in car internals. Lastly, current speed is controlled by a driver and therefore we would like to change it- through a setter method and get it- through a getter method.

Properties

In most langauges, getter and setter methods is how you would implement encapsulation, but C# is unlike most languages. It supports a concept of a property- a field, getter and setter methods in one member.

A typical property in C# looks like this:

public Type MyProperty{get;set;}

Both getter and setter methods take the access modifier from the property member. They can, however, reduce the access individually. For example if you want to change the value, but only internally, you can:

public Type MyProperty{get;private set;}

In other case you might want a read-only property. The value of a readonly property can only be set during object initialization.

public Type MyProperty{get;}

Let's refactor the Car:

public class Car
{
    public double X{get;}
    public double Y{get;}
    public double Z{get;}
    public string Body{get;}
    public float CurrentSpeed{get;set;}
    private readonly float _maxSpeed;
    // Ctors

    // Methods
}

The above code is equivalent to the one we had before. However, note 2 other differences- both in this line:

private readonly float _maxSpeed;

readonly is a modifier which can be applied to fields and make them read-only. In other words, it makes fields immutable- prevents changing their value both internally and externally.

We renamed MaxSpeed to _maxSpeed because that's the conventional way of naming fields.

Inheritance

Inheritance is a second pillar of OOP. As the name implies, it allows a child to take members of a parent. Essentially, inheritance is a great way to model an is-a relation both clarifying and reusing a good chunk of code.

Batman/Batwoman need more ways to travel- a car is not enough. They need a bike and a ship. They are all vehicles, but at the same time each introduce something new of their own. A car has 4 wheels, a bike has 2 wheels, a ship has an anchor. Such a scenario is a perfect use case for inheritance.

Let's move all that a Car has to a Vehicle class.

public class Vehicle
{
    public double X{get;}
    public double Y{get;}
    public double Z{get;}
    public string Body{get;}
    public float CurrentSpeed{get;set;}
    private readonly float _maxSpeed;

    public Car(string body, float maximumSpeed)
    {
        Body = body;
        MaximumSpeed = maximumSpeed;
    }

    void MoveTo(double x, double y, double z)
    {
        X = x; 
        Y = y; 
        Z = z;
    }
}

Inheritance in C# is done using a colon(:) symbol. When we inherit another class, the child class constructor must choose which constructor of a parent will be used during the initialization of a new object and forward data to it. This is done using a base keyword (parent ctor is called a base ctor). This isn't necessary only when we have a default constructor in a parent. In C#, only one class can be inherited. However, a child class can be inherited as well, infinitely deep.

Car:

public class Car: Vehicle
{
    // Oversimplified
    public string[] Wheels{get;}

    public Car(string body, double maximumSpeed, string[] wheels): base(body, maximumSpeed)
    {
        Wheels = wheels;
    }

    private void ValidateWheels(string[] wheels)
    {
        if(wheels.Length != 4)
        {
            throw new Exception($"A car must have exactly 4 wheels, but was {wheels.Length}");
        }
    }
}

Creating a new car now requires wheels:

var wheels= new string[]{"1", "2", "3", "4"};
var batmobile = new Car("Batmobile Black", 400, wheels);

A bike will inherit a vehicle in a very similar way:

public class Bike: Vehicle
{
    // Oversimplified
    public string[] Wheels{get;}

    public Bike(string body, double maximumSpeed, string[] wheels): base(body, maximumSpeed)
    {
        ValidateWheels(wheels);
        Wheels = wheels;
    }

    private void ValidateWheels(string[] wheels)
    {
        if(wheels.Length != 2)
        {
            throw new Exception($"A bike must have exactly 2 wheels, but was {wheels.Length}");
        }
    }
}

Batbike

Bike and Car classes look awfully similar. You might be tempted to reuse Car code in a bike refactoring them. However, stop for a minute and reconsider. Does this logically make sense a bike is a car? NO! Both are vehicles- that's what they have in common, but one is not the other. Just because in code they might look similar, they are logically unrelated. And therefore we should not model inheritance between a Bike and a Car classes.

Duplicate code is not worse than illogical code. Even though there would be less code and it would still work as before from the point of view of a machine- we're not writing code for machines. We are writing code for humans. The whole point of OOP is to simplify things. Just because you are using a tool from the OOP toolbox, does not mean that you are doing it properly. Just because you are using a hammer doesn't mean that you will hit the nail.

Virtual

In some cases, a child might need to override the behavior of a parent. A ship class is a good example of that. A ship has an anchor. If an anchor is set- a ship won't move.

When a parent provides a method with a default behavior for children, but some children might want to override it- you should add virtual modifier to that method.

Let's make Vehicle.MoveTo method virtual:

public virtual void MoveTo(double x, double y, double z)
{
    X = x;
    Y = y;
    Z = z;
}

Ship class, overriding MoveTo method will look like this:

public class Ship: Vehicle
{
    private _isAnchored = false;

    public Ship(string body, double maximumSpeed): base(body, maximumSpeed)
    {
    }

    public override void MoveTo(double x, double y, double z)
    {
        if(_isAnchored) return;

        base.MoveTo();
    }

    public void SetAnchor()
    {
        _isAnchored = true;
    }

    public void RetrievedAnchor()
    {
        _isAnchored = false;
    }
}

Abstract Classes and Methods

Can a Vehicle exist by itself? No- we have more specific classes which complete it. Abstract- is a keyword used to specify an incomplete thing. It can be applied to both classes and methods.

Vehicle should be an abstract class. It provides a base structure for all vehicles, but does not give enough meaning by itself.

MoveTo should be an abstract method. It has to be inside every Vehicle, but each vehicle will implement it in its own way.

Our Vehicle class will now look like this:

abstract class Vehicle
{
    // Properties
    // Ctor

    public abstract void MoveTo(double x, double y, double z);
}

Please note that we didn't provide any body to a method MoveTo. After all, we won't implement it in a Vehicle; Child classes will provide the implementation.

If you try to write new Vehicle- you will get an error message: Cannot create an instance of abstract class. This is the correct behavior, Vehilce is an incomplete parent and we should instead initialize a child which completes the parent.

Polymorphism

Polymorphism is the third pillar of OOP. It says that objects, deriving from the same thing can be treated either as concrete or as a base type. In other words- the same object can have multiple (poly) shapes (morph).

In practice, this means that we can create any vehicle by assigning it to a...

... generalised type

Vehicle batship = new Ship("Batship Black", 300);

... or a specialized type

Ship batship = new Ship("Batship Black", 300);

Regardless of definition, we can still call shared members of a Vehicle class has, for example:

batship.MoveTo(1,1,0);

The same applies to a car and any other vehicle:

Vehicle batmobile = new Car(...);
Car batmobile = new Car(...);

A batmobile can be moved as well:

batship.MoveTo(1,1,0);

You might wonder: "We can choose either type- so what?". Both a car and a ship have 2 forms, 1 of which is shared. The beauty of it all is that regardless of the two moving in their own ways, in order to move both, we don't need to care whether it is a Car or a Ship.

If our hero wanted to call all Vehicles at their location, they could:

Vehicles[] vehicles = {batship, batmobile};
foreach(var vehicle in vehicles)
{
    vehicle.MoveTo(heroX, heroY, heroZ);
}

We moved both a batship and a batmobile without caring which one we are moving, because they can be treated as the same type- Vehicle.

So what? Imagine if you didn't have the ability to override a MoveTo method and would need to pick the right implementation based on the class an object implements. You would need at least one if statement! Polymorphism ensures simplicity and fluidity of code by eliminating the need to care about the specialized types.

Abstraction

Abstraction is the fourth pillar of OOP. It is the idea: in order to interact with objects, we don't need to know about their implementation.

Some say that by itself abstraction does not add anything and claim there are just 3 pillars. But abstraction is like a wiring point combining the remaining pillars:

  • Encapsulation helps abstraction by maximizing the amount of private stuff
  • Inheritance allows generalisation. It's one of the ways of implementing abstraction, because in order to use a child all we have to care about is the parent (polymorphism).

--

What does a batman and batmobile have in common? They can both move. If we wanted to command both in the same way, we could derive both from the same base class Mover. However, a batmobile is also vehicle and due to being able to inherit only one class in C#, we cannot achieve what we want yet.

Inheritance is a poor abstraction, because it often provides implementation with it. What if we don't want any implementation? Is there such a thing as a "pure abstraction"?

Interface

When classes do the same thing, but differently (and there is no default implementation), we should use an interface. Just like an abstract class, it will include methods without a body, but unlike a class- it will not allow any implementation (fields or methods with a body).

Interface name should be based on the behavior it abstracts away. We want to move something- thus Moveable. In C#, it's conventional to name an interface with a capital I at the start. IMoveable:

public interface IMoveable
{
    void MoveTo(double x, double y, double z);
}

Similar to inheritiance, classes which implement an interface will derive from it (:).

A Batman class could look like this:

public class Batman: IMoveable
{
}

If you try to compile this code, it will error: Interface member IMoveable.MoveTo is not implemented. Interface is a contract and a contract must be fulfilled, else your code won't compile. In order to fulfill the contract IMoveable, we need to implement MoveTo method:

public class Batman: IMoveable
{
    // Properties
    // Ctors
    public void Move(double x, double y, double z)
    {
        Console.WriteLine("Batman is running");
    }
}

Let's apply the interface to Vehicle as well:

public class Vehicle: IMoveable
{
    // properties
    // ctors

    public abstract void MoveTo(double x, double y, double z);
}

Implementing an interface does not require any special keyword. And as you noticed, given we are in an abstract class, we're not forced to implement an interface as long as we have an abstract method for it.

So what? Regardless of being two very different things, batman and batmobile can meet in the same location with a single command:

IMoveable moveables = new []{batman, batmobile};

foreach(var moveable in moveables)
{
    moveable.MoveTo(1,1,0);
}

This illustration also applies to polymorphism. As mentioned before, abstraction is related to all other pillars.

Another benefit of interfaces is that you can implement as many of them as you want. In our case, another common thing that batman and batmobile have in common is that both are locatable. Let's create ILocatable interface:

public interface ILocatable
{
    public double X{get;}
    public double Y{get;}
    public double Z{get;}
}

Implementing the two interfaces will look like this:

public class Batman: IMoveable, ILocatable
{
    public double X{get; private set;}
    public double Y{get; private set;}
    public double Z{get; private set;}

    // Properties
    // Ctors
    public void Move(double x, double y, double z)
    {
        Console.WriteLine("Batman is running");

        X = x;
        Y = y;
        Z = z;
    }
}
Batman and Batmobile

Isn't it a bit strange to see an interface with data (properties)? Actually, interface is an abstraction on behavior and properties are just setter and getter methods, therefore interface still works. Don't mix properties on an interface with properties on a class. The difference is that properties on an interface will need to be replicated on every class that implements it, unlike properties on a class.

Interface is hands down the best abstraction, because it does not come with any implementation overhead. When in doubt of whether you should use inheritance or an interface, you should probably pick a safe route- an interface.

Composition

Before finishing this lesson, it's worth talking about composition- making bigger objects from smaller ones. There are 2 use cases for composition:

  • Some values are just too closely related together and they fit better under another object
  • Class is too complex and its behavior needs to be split in components

In our scenario, x, y and z form a concept of a location. Therefore, instead of passing 3 variables all the time, we could group them under one class- Location:

public class Location
{
    public double X{get;}
    public double Y{get;}
    public double Z{get;}

    public Location(double x, double y, double z)
    {
        X = x;
        Y = y;
        Z = z;
    }
}

The implementation of the two interfaces will now be simplified:

public class Vehicle: IMoveable, ILocatable
{
    // properties
    public Location Location{get; private set;}
    //...
    // ctor

    public void MoveTo(Location location)
    {
        Location = location;
    }
}

Final Words

Object Oriented Programming is not the answer to all the problems. It is just another tool in your toolbox to solve problems. In different scenarios you will need different tools and you should be open to learning them as long as they solve your problem.

Just using classes and objects don't make you a proficient user of OOP. The essence of OOP lies in understanding that it is not meant for a machine, but for another person to understand. There is another acronym for OOP- POO. Programming fOr Others. It's all about simplicity, modularity and empathy of a reader.

Homework

Your application needs logging. Logging can have 2 sources:

  • File
  • Console

You can log at different levels as well:

  • Info
  • Warn
  • Error

In lesson 4 (recipe application of winforms) add logging for:

  • Every exception thrown should also be logged as warning
  • Unhandled exception should be logged as error
  • Based on logs it should be clear what happened throughout the program

Implement both loggers, but choose 1 which will be used consistently throughout the application.

Did You Understand the Topic?

  • Why do we need OOP?
  • What is the difference between a class and an object?
  • What is the difference between a static and non-static classes?
  • Where should you validate an new object state?
  • How many constructors can a class have at most? At least?
  • What happens with the values of fields if you don't set them in a constructor yourself?
  • Can 2 objects of the same class coexist?
  • Why would anyone say there are just 3 pillars of OOP?
  • What is encapsulation?
  • What power does inheritance carry?
  • When should we apply inheritance?
  • Give an example of polymorphism.
  • How is abstraction similar to polymorphism and encapsulation?
  • What is the best Abstraction?
  • When should you use an Interface?
  • How to prevent class from being initialized? (2 ways)
  • Why do we need virtual methods? Are methods virtual by default?
  • Why is the default access modifier on a class private?
  • In OOP sense, is car wreck a vehicle?

Clone this wiki locally