2

I am relatively new to C# and OOP concepts in general, but am building a standalone application and have run into a question and want to make sure I'm doing it the "right way".

I have a few different types of windows, each type of which I have implemented as its own class. During run time, all of the currently open windows, regardless of type, will need their values/parameters updated every 500ms.

I understand that an interface specifies a contract for what methods a class must implement. What I'm not sure about, is if an IUpdatable interface makes sense for this situation, or if I should have each class implement a method called Update without the interface. I understand that both would accomplish what I'm trying to do. However, I'm trying to understand why I would choose to go with one approach vs the other. I appreciate any feedback

3
  • An interface separates two different worlds: the clients of the interface from the implementations of the interface, where neither should know about the other (in a perfect world, at least). Only you know whether it makes sense for your use case. Oct 21, 2021 at 18:12
  • Does this answer your question? Can't I just use all static methods?
    – gnat
    Oct 21, 2021 at 21:00
  • Sounds like you plan to implement .Update() in each class either way, so that's not really part of the question. Instead, seems like you're basically just asking if you should define an interface that connects the methods.
    – Nat
    Oct 22, 2021 at 0:02

5 Answers 5

3

Yes, an interface defines a contract where the implementor of the interface must provide an implementation for all interface members. Usually you would prefer adding dependencies to an interface type over dependencies to concrete types. Which is especially interesting for you is the benefit of enabling polymorphism by introducing interfaces like IUpdatable.

1) The compiler will report an error when an implementation is missing
It's impossible to forget to provide an implementation of a public member when the defined member is defined in the imlemented interface. When a class must implement a certain API in order to be able to participate in a certain logic, an interface can gurantee this. For example when you need to implement a media player, the interface IMediaPlayer will ensure that all the required members like Play(), Pause(), Stop(), ElapsedTime are implemented. The author does no longer need to remember all the required members that make a valid media player.
More important, the compiler will force the author to implement the members or the code won't compile.
This adds welcomed robustness to the application.

2) Get IDE support by using code generators
Declaring a type to implement an interface, allows to use code auto-generation. All method bodies and properties are generated automatically. That adds comfort.

3) Interfaces help to add extensibility to the application
Following the OO principles like the Dependency Inversion principle of SOLID principles, helps to add extensibility to your application. Developing against interfaces (or abstractions of the concrete type) allows the code to be independent of the actual implementations and therefore reusable and easy to extend (adding new types)

Bad:

// Only operates on MainWindow instances.
// This implementation makes extending the application difficult.
// If a new window type is added to the application or the MainWindow type is replaced, 
// this method needs to be changed or we have to provide overloads.
public void UpdateWindow(MainWindow window)
{
  window.Update(data);
}

Good:

// Operates on every type as long it implements IUpdatable.
// This is a highly reusable version that adds extensibility to the application: 
// If new implementations are added to the application, 
// this method does not need any changes.
public void UpdateWindow(IUpdatable updatable)
{
  updatable.Update(data);
}

4) Interfaces enable polymorphism
At run time, objects of an interface implementation may be treated as objects of the same type in places such as method parameters and collections or arrays.
This means you are able to provide a single implementation that for example iterates over a collection of particular interface typed elements, where each element is treated anonymously as the common base type - the interface type.

Bad:
In order to iterate over all different types of Window to call the Update() defined on the derived type, we need to know every possible Window subtype. Such code is difficult to extend as adding new types require to modify the code. Appart from this, polymorphism is not possible.

class MainWindow : Window
{
  public void Update(object data)
  { ... }
}

class SettingsWindow : Window
{
  public void Update(object data)
  { ... }
}

class DependingType
{
  List<Window> Windows { get; }
  
  private void UpdateAllWindows()
  {
    object newData = new object();

    // Needs modification in case a new Window type e.g., UserProfileWindow is added to the application
    foreach (Window window in this.Windows)
    {
      if (window is MainWindow mainWindow)
      {
        mainWindow.Update(newData);
      }
      else if (window is SettingsWindow settingsWindow)
      {
        settingsWindow.Update(newData);
      }
    }
  }
}

Good:
Since the actual Window types are all unknown (anonymous), this solution does only depend on the implemented interface of those types. Such a solution is highly extensible. Furthermore, it enables polymorphism.

interface IUpdateable 
{ 
  void Update(object data); 
}

class MainWindow : Window, IUpdatable
{
  public void Update(object data)
  { ... }
}

class SettingsWindow : Window, IUpdatable
{
  public void Update(object data)
  { ... }
}

class DependingType
{
  List<IUpdatable> Updatables { get; }
  
  private void UpdateAllWindows()
  {
    object newData = new object();

    // Use polymorphism to invoke Update() on each type.
    // This needs no modification in case we add e.g. a UserProfileWindow
    foreach (IUpdatable updatable in this.Windows)
    {
      updatable.Update(newData);
    }
  }
}

5) Interfaces help to improve testability
APIs that depend on interfaces, are easy to test. The interface allows to fake behavior of the tested method or to mock the type.

Bad:
The following example depends on the concrete class FileSystemReader and therfore always executes the reading the complete file system. This takes time, makes our test slow and does not provide anything to the effective unit test.

class FileSystemReader
{
  // Enumerate the complete! filesystem
  public List<string> GetAllFilePaths()
  { 
    var filesystemPaths = new List<string>();
    foreach (DriveInfo drive in DriveInfo.GetDrives())
    {
      filesystemPaths.AddRange(Directory.GetFileSystemEntries(drive.RootDirectory));
    }

    return filesystemPaths;
  }
}

class MainWindow : Window
{
  public AddPrefixToAllFiles(string fileFilter, string prefix, FileSystemReader fileSystemReader)
  {
    var allFilePaths = FileSystemReader.GetAllFilePaths();
    
    // TODO::filter allFilePaths for the requested files
    List<string> filteredFilePaths = FilterFilesystemItems(fileFilter, allFilePaths);
    
    // This is the logic we want really want to test
    foreach (string filePath in filteredFilePaths)
    {
      filePath = prefix + filePath;
    }
  }
}

class Test
{
  public void TestMainWindowAddPrefixToAllFiles()
  {
    var mainWindow = new MainWindow();

    // Since AddPrefixToAllFiles only accepts a concrete implementation, we can't fake it
    FileSystemReader fileSystemReader = new FileSystemReader();

    // Takes very long to execute as we are also implicitly testing the FileSystemReader class
    List<string> prefixedFiles = mainWindow.AddPrefixToAllFiles("*.txt", "log_", fileSystemReader);

    // TODO::Assert prefixedFiles elements
  }
}

Good:
The following version depends on the IFileSystemReader interface rather than on the concrete FileSystemReader class. This allows to provide a FileSystemReaderFake fake implementation to replace the longrunning FileSystemReader class.

// The interface definition to enable fakes
interface IFileSystemReader
{
  List<string> GetAllFilePaths();
}

// The fake class that provides a performance friendly implementation of IFileSystemReader
class FileSystemReaderFake : IFileSystemReader
{
  public List<string> GetAllFilePaths()
  {        
    // Fake the filesystem enumeration
    return new List<string> { "first.txt", "second.txt", "third.txt" };
  }
}

// Now depends on the IFileSystemReader interface
class MainWindow : Window
{
  public AddPrefixToAllFiles(string fileFilter, string prefix, IFileSystemReader fileSystemReader)
  {
    var allFilePaths = fileSystemReader.GetAllFilePaths();
    
    // TODO::filter allFilePaths for the requested files
    List<string> filteredFilePaths = FilterFilesystemItems(fileFilter, allFilePaths);
    
    // This is the logic we really want to test
    foreach (string filePath in filteredFilePaths)
    {
      filePath = prefix + filePath;
    }
  }
}

class Test
{
  public void TestMainWindowAddPrefixToAllFiles()
  {
    var mainWindow = new MainWindow();

    // Since AddPrefixToAllFiles() now accepts an interface, we can provide a fake implementation
    IFileSystemReader fileSystemReaderFake = new FileSystemReaderFake();

    // Executes fast as we are only testing the AddPrefixToAllFiles logic
    List<string> prefixedFiles = mainWindow.AddPrefixToAllFiles("*.txt", "log_", fileSystemReaderFake);

     // TODO::Assert prefixedFiles elements
  }
}
2
  • 2
    Very comprehensive response. It sounds like interfaces just make life easier all around. Makes me seriously wonder why my professor barely touched on them in university (and question the quality of my formal education LOL). I really appreciate your response. The part about not needing to know the object type as long as it implements IUpdatable was extremely helpful to me
    – adc395
    Oct 21, 2021 at 22:01
  • @adc395: I don't know your professor, but most professor's I met in the past were good at theory, but not at craftmenship.
    – Doc Brown
    Oct 22, 2021 at 19:58
3

Interfaces are for increasing generalizability.

Let's say you need to set some parameter named Foo in all of your windows. You could write a method for each type of window. If you have three types of window, you'd need three methods:

void SetFoo(WindowType1 window, string text)
{
    window.Foo = text;
}

void SetFoo(WindowType2 window, string text)
{
    window.Foo = text;
}

void SetFoo(WindowType3 window, string text)
{
    window.Foo = text;
}

If you gave all of your window types an IUpdateable interface, you'd only have to write a single, generalized method:

void SetFoo(IUpdateable anyWindow, string text)
{
    anyWindow.Foo = text;
}

If you don't need the generalizability, you don't need the interface. Something tells me you do.

1
  • Thank you. One of the biggest hurdles I am having with OO is thinking in terms of generalizability but this helped
    – adc395
    Oct 21, 2021 at 21:56
2

In your scenario implementing the interface on each of your forms would help because it would allow you to iterate over all your forms, treat them as IUpdatable and call update on each.

Say you have this list:

List<IUpdatable> updatables = List<IUpdatable>();

and whenever one of your forms is created you add it to this list. You can do this for each type of form because each is declared as implementing IUpdatable. Then you can do this:

foreach (IUpdatable updatable in updatables)
{
    updatable.Update();
}

With just a bunch of Form descendants you would not be able to do this because Form does not have an Update() method.

1
  • This is exactly what I have planned. I appreciate your feedback!
    – adc395
    Oct 21, 2021 at 21:48
1

With your current architecture,

Go with the interface

I'm going to make these assumptions:

  • Some other class(es) will be calling the Update method
  • The IUpdatable classes know where to get the updated parameters/values

With an interface, the class calling the Update method needs to know exactly nothing about the windows that it is telling to update. You could essentially have a list of Windows (or other things later!) with List<IUpdatable> and loop througg to call each update method. This will make it smoother to add more windows later.

Better yet,

Use a better design

Not saying you should scrap your stuff, but maybe move towards MVVM or use it for future projects. In MVVM, you'd have a thread/class/whatever updating the actual Models and those changes bubble up through Data Bindings out to the View. Then the Models and the data don't need to know anything about the View. This sounds more like what you're trying to do.

2
  • Thanks for the feedback! I learned about MVVM after I started the project. I'm not too far in that I can't change it up, however, the data comes from an external system that I interact with and retrieve via an external DLL, so I kind of convinced myself that MVVM might not apply. But admittedly I think I am just overwhelmed trying to understand how to implement the MVVM architecture
    – adc395
    Oct 21, 2021 at 19:37
  • MVVM has nothing to do with interfaces. It has nothing to do with the question "class vs interface".
    – BionicCode
    Oct 21, 2021 at 19:44
-1

There is yet another possibility: Implement the update functionality in a base window class. Every other window class that needs this updates can then be derived from this base class and will inherit the functionality.

As an example let's assume that the base for all windows is the class Window:

 ┌────────────────┐
 │ Window         │
 └────────────────┘
         ▲
 ┌───────┴────────┐
 │ UpdatableWindow│
 └────────────────┘
         ▲
         ├───────────────────┐....
 ┌───────┴────────┐  ┌───────┴────────┐
 │ MyWindow1      │  │ MyWindow2      │ ....
 └────────────────┘  └────────────────┘
public abstract class UpdatableWindow : Window
{
    public Update(parameters)
    {
        // TODO: implement
    }
}

public class MyWindow1 : UpdatableWindow { }
public class MyWindow2 : UpdatableWindow { }

For this approach to work, it is necessary to implement the updating in a generic way. I don't know whether this is possible in your case.

3
  • 3
    The downside is that every modern OO language forbids multi-inheritance. You can't implement a class MainWindow : Window, UpdatableForm.
    – BionicCode
    Oct 21, 2021 at 19:43
  • Yeah my windows already inherit from base class Window, so my thought process was that I need to find a way to make them all updatable - presumably via an interface. Thanks for your feedback though
    – adc395
    Oct 21, 2021 at 21:52
  • @adc395 No, but you can inherit Window <- UpdatableForm <- MainWindow (as I have shown in the example). No multiple inheritance required. But if you need multiple inheritance, now you can kind of have it through the new interface default implementations, which implement the concept of "traits". Oct 22, 2021 at 11:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.