mardi 9 novembre 2021

Avoid infrastructure code in domain model

this will be more of a Discussion about Object-Oriented Design than a technical question. I will first state an exemplary use-case and then my thoughts on it. I will use simplified C#.

Imagine a WPF-GUI application for managing firmware updates for multiple products. Let the products be Mouse Driver, Keyboard Driver, and Speakers Driver. The GUI should:

  • Display the available updates in a list view with information such as Name, Description and FilePattern.
  • Have an "Update"-Button for each list item that retrieves the patch-file and applies it to the device.

A naive design would be to represent the products as an enum.

enum ProductType
{
    Mouse,
    Keyboard,
    Speakers
}

and to populate the view-model data with a switch statement:

switch(product.Type)
{
    case Mouse:
        Name = "Mouse X100 7.1"
        FilePattern = ".asdf"
        Description = "The X100 7.1 surround sound gaming mouse."
        break;
    case Keyboard:
        Name = "20000-DPI Gaming Keyboard"
        FilePattern = ".qwertz"
        Description = "The best gaming keyboard"
        break;
    case Speakers:
        Name = "Cheap Speakers"
        FilePattern = ".msi"
        Description = "Some cheap speakers."
        break;
}

I think we can agree that following this design would be a bad practice, as adding a new product would require to extend possibly multiple switch statements. Applying the "Replace Conditional with Polymorphism" refactoring would lead to the following solution:

class Product
{
    string Name;
    string FilePattern;
    string Description;
}

class Mouse: Product
{
    Name = "Mouse X100 7.1";
    FilePattern = ".asdf";
    Description = "The X100 7.1 surround sound gaming mouse.";
}

class Keyboard: Product 
{ .... }

class Speakers: Product
{ ... }

These will be our domain objects. Let's assume we want to keep our domain model clean and free of infrastructure concerns, as proposed in "Clean Architecture" (Robert C. Martin), the Ports & Adapters architecture, and others.

Now let's also assume that the different products require different strategies to download and apply a patch (e.g. different servers/protocols). The code for those strategies will be very infrastructure-heavy, as it depends on external systems.

Now comes the time to hit the "Update"-Button and to apply a patch. In the GUI class we have the Product object and need to trigger the download and update procedure.

To implement this behavior, we could now e.g. use Template-Method:

abstract class Product
{
    string Name;
    string FilePattern;
    string Description;

    abstract void Update();
}

or Strategy-Pattern :

class Product
{
   private IUpdateStrategy updateStrategy;

   string Name;
   string FilePattern;
   string Description;

   void Update()
   {
       updateStrategy.Update();
   }
}

Either way we have now spoiled the clean domain model with infrastructure concerns. Even if those might be abstracted away by the IUpdateStrategy-interface, unit-testing code involving the Products now becomes annoying as we have to fake the strategy objects. In my opinion, calling the update strategy should not happen in the domain- but only in the application-layer. A way to prevent this would be to not include the update strategy in the Product but to select it based on the product's type:

if (product is Mouse mouse)
{
    mouseUpdater.Update(mouse);
}
else if (product is Keyboard keyboard)
{
    keyboardUpdater.Update(keyboard);
}
...

This would bring us back to if-else or switch statements that we tried to avoid by using polymorphism.

Another way I see would be to build up some form of registry that links a Product with its update-strategy:

class Registry (simplified)
{
    Dictionary<Product, IUpdateStrategy> table;
    void Register(Product product, IUpdateStrategy updateStrategy)
    {
        table[Product] = updateStrategy;
    }
}
...

This adds an extra layer of complexity, especially as the registry needs to be populated first.

I'm very interested in hearing your ideas on this topic.

Aucun commentaire:

Enregistrer un commentaire