Part 4 — Hiding and Inheritance

Hiding

Mechanisms of hiding in the language: an additional layer of access protection to the class’s data and methods. Hiding, as opposed to encapsulation, refers to techniques for partially restricting access to the class’s state. The C# language features ensuring this include:

  • Access modifiers for class members.
  • Getters/Setters mechanism.
  • Indexers.
  • Explicit interface implementation.
  • Events mechanism. Events allow for a thread-safe wrapper around a delegate and protect the chain of subscribers from being reassigned from outside the class.

Hiding is essential to shield users of our class from using it in a way that violates its integrity or leads to incorrect operation.

For example, a property might lack a {set;} method, making it impossible to assign from outside. Or a protected member, accessible only within the class and its descendants, which can be overridden if it’s virtual. But it’s not accessible from the outside.

Inheritance

  • To provide an abstraction for implementors of derived classes, to restrict developers.
  • To allow for reusing base functionality.

Inheritance is very deceptive by nature. When asked why we need inheritance, the immediate response might be: for code reuse.

This is almost the correct answer. Indeed, inheritance allows us to extend the behavior of a base class without rewriting its logic. However, the crux of the matter is that inheritance represents the strongest type of relation between classes, which can be hard to break in an already written and complex application.

The true essence of inheritance manifests in large codebases: to create a design that is hard to break. In massive projects, dozens or even hundreds of people might be working, while there could be just one architect.

By including inheritance into the system’s design, you glue class relationships that will be difficult to alter in the future.

Inheritance indeed allows for easy extension of existing classes but within strict confines. A typical problem that can be solved with inheritance: we have an algorithm where parts of it may vary significantly. A straightforward solution would be to write an abstract class, implementing methods in it that are built on certain abstract or partially abstract methods which, in turn, are mandated to be implemented in the derived classes. But if there are many derivatives and you still have to write the methods’ implementations, wouldn’t it be simpler to assemble any method chain using something similar to a Fluent Interface, then pass the necessary delegates depending on the situation?

The described solution has one downside: it’s easy to break. In the case of an abstract class, the solution will be solid. No one would think of changing an abstract class that has dozens of derivatives and refactoring a huge part of the project, thus protecting the abstract algorithm implemented within.

Whether to use inheritance or not is up to you. Taking the classic example of shapes, where there is a Shape with three descendants: Circle, Square, and Triangle, students are prompted to write object-oriented code to calculate the areas and perimeters of these shapes.

Traditionally, the task needs to be implemented as follows:

public class Shape
{
// A few example members
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }

// Virtual method
public virtual void Draw()
{
Console.WriteLine("Performing base class drawing tasks");
}
}
public class Circle : Shape
{
public override void Draw()
{
// Code to draw a circle…
Console.WriteLine(“Drawing a circle”);
base.Draw();
}
}

public class Rectangle : Shape
{
public override void Draw()
{
// Code to draw a rectangle…
Console.WriteLine(“Drawing a rectangle”);
base.Draw();
}
}

public class Triangle : Shape
{
public override void Draw()
{
// Code to draw a triangle…
Console.WriteLine(“Drawing a triangle”);
base.Draw();
}
}

However, what prevents us from implementing it as the following?

public class Shape
{
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
public ShapeType Type { get; set; }
public void Draw()
{
var drawingText = Type switch
{
ShapeType.Circle => "Drawing a Circle",
ShapeType.Square => "Drawing a Square",
ShapeType.Triangle => "Drawing a Triangle",
_ => throw new NotImplementedException(),
};

Console.WriteLine(drawingText);
}
}

In this case, we don’t hardcode the types of shapes into the program’s design using inheritance. On the one hand, this simplifies the modification and addition of new shapes. On the other hand, it will be easier to remove any given shape or to remove shapes altogether.

This is usually what they forget to tell students, and when these students end up in real-world projects, they try to use inheritance everywhere they can reach, which can significantly increase technical debt and make the system’s design very complex. Inheritance should always be considered in conjunction with polymorphism (the next article in series). While inheritance creates a strong relation, polymorphism allows for degrees of flexibility. In the C# language, technically, inheritance is possible:

  • From interfaces without implementation.
  • From interfaces with a default implementation (default interface implementation).
  • From a class.
  • From an abstract class.

Interface vs Abstract class

A frequent question in developer interviews, but the answer depends on the context, i.e., what problem we are trying to solve.

If our goal is to separate a code, separate classes for interaction through abstraction, and configure an IoC container, then interfaces are a good tool.

Everything an interface can do, an abstract class can do too, except for multiple inheritance.

However, abstract classes address a different problem: they can implement behavior. It’s somewhat incorrect to compare the behavior implementation in abstract classes with the Default interface implementation (explained further). If you need to implement some basic behavior for specific methods, allowing or disallowing its override — an abstract class is a good solution.

Default interface implementation

C# 8 introduced the ability to have default implementations in interfaces. Comparing this language feature with abstract classes purely based on capabilities isn’t entirely correct. This was implemented for backward compatibility, for instance, some system implements a plugin mechanism, and to avoid breaking plugins developed by third-party developers (i.e., not forcing them to implement new interface members) when pushing a new update, the ability to provide a default implementation directly in the interface was added. Using this feature is not recommended in other cases.

Perhaps, one of the most confusing topics for beginners in the C# language.

Sealed classes

Sealed classes can be related to inheritance and encapsulation; the ‘sealed’ keyword explicitly restricts the ability to inherit from them. This is done to protect existing code and prevent class substitution with its descendants. Such protection is essential for critical areas in the program, for instance, the ‘string’ sealed class is there to ensure that potential breaking changes into “string” don’t break descendant classes, i.e., to ultimately enhance the reliability of the program.

If you need to create additional functionality around ‘string’, implement your extension method or explicitly wrap the class.

--

--