piątek, 22 listopada 2019

SOLID - D jak Dependency Inversion principle

Ostatnia z zasad SOLID to Dependency Inversion principle - zasada ta mówi, że wysokopoziomowe moduły nie powinny zależeć od modułów niskiego poziomu. Zależności pomiędzy tymi modułami powinny być realizowane za pomocą abstrakcji. Abstrakcja nie powinna zależeć od implementacji, a implementacja powinna zależeć od abstrakcji.

Tradycyjne podejście komponenty wysokopoziomowe zależą od niskopoziomowych. Przykładowy kod:

    public class AutomaticWashingMachine
    {
        public void StartWashingCycle()
        {
            var engine = new Engine();
            var heater = new Heater();
            //all other required objects
            //code to process washing cycle
        }
    }

    public class Heater
    {
        private TemperatureSensor temperatureSensor;

        public Heater()
        {
            temperatureSensor = new TemperatureSensor();
        }

        public void HeatWater(double requiredTemperature)
        {
            while(Math.Abs(requiredTemperature - temperatureSensor.GetCurrentTemperatur()) < 0.1)
            {
                Console.WriteLine("Rising water temperature...");
            }
        }
    }

    public class TemperatureSensor
    {
        private double currentWaterTemperature = 10;

        public double GetCurrentTemperatur()
        {
            return currentWaterTemperature += 1;
        }
    }

    public class Engine
    {
        public void On()
        {
            Console.WriteLine("I turn the drum...");
        }

        public void Off()
        {
            Console.WriteLine("Stoping engine...");
        }
    }

Powyżej przykład kodu oprogramowującego działanie pralki automatycznej. Powyższy kod nie jest zgodny z zasadą DIP. Oprócz tego posiada następujące wady:

  • trudno napisać do niego testy jednostkowe. Wynika to z faktu, że aby przetestować przycisk włączający pralkę należy stworzyć cały system - nie możemy zaślepić chwilowo grzałki i silnika (Mocki).
  • stałe zależności w kodzie, trudno będzie wymienić czujnik temperatury na inny, gdyż obecny jest mocno powiązany z grzałką.
Powyższy kod można w bardzo łatwy sposób przerobić do kodu opartego o abstrakcję (interfejsy). Zobaczmy jak może to wyglądać:

    public class AutomaticWashingMachine
    {
        private readonly IEngine engine;
        private readonly IHeater heater;

        public AutomaticWashingMachine(IEngine engine, IHeater heater)
        {
            this.engine = engine;
            this.heater = heater;
        }

        public void StartWashingCycle()
        {
            //all other required objects
            //code to process washing cycle
        }
    }

    public interface IHeater
    {
        void HeatWater(double requiredTemperature);
    }


    public class Heater : IHeater
    {
        private readonly ITemperatureSensor temperatureSensor;

        public Heater(ITemperatureSensor temperatureSensor)
        {
            this.temperatureSensor = temperatureSensor;
        }

        public void HeatWater(double requiredTemperature)
        {
            while (Math.Abs(requiredTemperature - temperatureSensor.GetCurrentTemperatur()) < 0.1)
            {
                Console.WriteLine("Rising water temperature...");
            }
        }
    }

    public interface ITemperatureSensor
    {
        double GetCurrentTemperatur();
    }

    public class TemperatureSensor : ITemperatureSensor
    {
        private double currentWaterTemperature = 10;

        public double GetCurrentTemperatur()
        {
            return currentWaterTemperature += 1;
        }
    }

    public interface IEngine
    {
        void On();
        void Off();
    }

    public class Engine : IEngine
    {
        public void On()
        {
            Console.WriteLine("I turn the drum...");
        }

        public void Off()
        {
            Console.WriteLine("Stoping engine...");
        }
    }

Powyższy kod zawiera sporo nowych interfejsów: IEngine, IHeater, ITemperatureSensor. Dzięki zastosowaniu interfejsów nie wiążemy poszczególnych modułów/klas z pralką automatyczną. Każdy komponent jest wymienny tak długo jak implementuje zadany interfejs. Takie podejście ułatwia implementowanie testów jednostkowych. W przypadku gdy chcemy wymienić jeden z komponentów na inny, także możemy to w prosty sposób zrobić bez konieczności przerabiania tony kodu.

Brak komentarzy:

Prześlij komentarz