SOLID – część 2. Zasada Otwarte Zamknięte

SOLID – mnemonik, który mówi jak pisać poprawnie programy w językach obiektowych. Podążanie za tymi zasadami znacznie poprawia czytelność i możliwość utrzymywania kodu. W tej części będzie opisana druga zasada: Zasada otwarte zamknięte.

W momencie kiedy stworzyliśmy już czołg zgodnie z zasadą jednej odpowiedzialności możemy bez problemu tworzyć nowy kod bez wchodzenia sobie z innymi współpracownikami w paradę.

Nowe zadania! Klient chce, aby czołg mógł jeździć szybciej. Zamiast gąsienic ma mieć kołka. Lufa ma mieć możliwość strzelania pociskiem lub tajną bronią. Dodatkowo czołg ma mieć Karabin. Do czołgu można zamontować dwa różne karabiny – szybko strzelający lub celny karabin. Klient chce mieć Długą Lufę w czołgu. Ma różnić się od zwykłej Lufy tym, że jej długość to 250 cm! Klient chce mieć czołg, które może być wyposażony albo w gruby pancerz albo lekki.

Zajmijmy się wpierw kółkami.

public class Czolg
{
	private readonly Kolka _kolka;

	public Czolg()
	{
		...
		_kolka = new Kolka();
	}
}

public class Kolka
{
	public void Jedz()
	{
		Console.WriteLine("Czołg szybko się przemiesza.");
	}
}

Kołka zamontowane. Duma bije na kilometr.

Aplikujemy SOLID

O – Open/Close Princliple (Zasada otwarte zamknięte)
Spójrzmy na to co mówi zasada otwarte zamknięte?

Program powinien być otwarty na rozszerzenie, ale zamknięte na modyfikacje.

Co to znaczy? Oznacza to, że jeżeli mamy napisany pewien kod nie powinien on już być zmieniany (chyba że zmienią się wymagania do tej części kodu). Naszym działaniem naruszyliśmy zasadę OCP. Klasa czołg zmieniła się. Zamiast pola Gąsienica ma pole Kółka.

Jak to można naprawić? Zastosujmy się teraz do „otwarty na modyfikacje”. Klasa Kółka i Gąsienica mają tą samą metodę. Stwórzmy więc dla nich wspólny interface:

public interface IJezdzace
{
	void Jedz();
}

Poprawmy klasy:

public class Gasienice : IJezdzace
{
	public void Jedz()
	{
		Console.WriteLine("Przechajales krotka odleglosc. Ale wjechales pod gorke.");
	}
}

public class Kolka : IJezdzace
{
	public void Jedz()
	{
		Console.WriteLine("Czołg szybko się przemiesza.");
	}
}

I stwórzmy czołg, w taki sposób, aby był otwarty na modyfikacje:

public class Czolg
{
	private readonly IJezdzace _naped;

	public Czolg()
	{
		...
		_naped = new Kolka();
	}
}

Teraz bez problemu możemy zmieniać Kółka z Gąsienicami. Jak widzimy interfejs bardzo otwiera nasz program na modyfikacje.

Zajmijmy się teraz pociskiem i tajną bronią. Pocisk i Tajna Broń mają tą samą funkcjonalność: mają niszczyć. Stwórz więc odpowiedni interface.

public interface ISiejacyZniszczenie
{
	void Zniszcz(); 
}

A teraz poprawmy kod Pocisku i zaimplementujmy Tajną Broń:

public class Pocisk : ISiejacyZniszczenie
{
	private readonly double _trafnoscPocisku;
	private bool _zostalZuzyty;
	private bool _zniszczylCel;

	public Pocisk()
	{
		_trafnoscPocisku = 0.5;
		_zniszczylCel = false;
		_zostalZuzyty = false;
	}

	public void Zniszcz()
	{
		if (!SprawdzCzyZostalZuzyty())
		{
			_zostalZuzyty = true;

			if (SprawdzCzyZniszczylCel())
				_zniszczylCel = true;
			else
				_zniszczylCel = false;
		}
		else
		{
			throw new PociskZostalJuzZuzyty();
		}
	}

	private bool SprawdzCzyZostalZuzyty()
	{
		return _zostalZuzyty == false;
	}

	private bool SprawdzCzyZniszczylCel()
	{
		var rand = new Random();
		return rand.NextDouble() < _trafnoscPocisku;
	}
}

public class TajnaBron : ISiejacyZniszczenie
{
	private bool _zostalZuzyty;
	private bool _zniszczylCel;

	public TajnaBron()
	{
		_zostalZuzyty = false;
		_zniszczylCel = false;
	}

	public void Zniszcz()
	{
		if (SprawdzCzyZostalZuzyty())
		{
			_zostalZuzyty = true;
			_zniszczylCel = true;
		}
		else
		{
			throw new PociskNieZostalZuzyty();
		}
	}

	private bool SprawdzCzyZostalZuzyty()
	{
		return _zostalZuzyty == false;
	}
}

Poprawmy lufę.

public class Lufa
{
	private ISiejacyZniszczenie pocisk;

	public virtual void LadujPocisk(ISiejacyZniszczenie pociskSiejacyZniszczenie)
	{
		if (!PociskJestZaladowany)
		{
			PociskJestZaladowany = true;
			pocisk = pociskSiejacyZniszczenie;
		}
		else
		{
			throw new PociskJestJuzZaladowany();
		}
	}
}

W tym momencie Lufa może przyjąć i Tajną Broń i zwykły Pocisk (tutaj można byłoby zastosować klasę abstrakcyjną).
Zajmijmy się teraz Lufą. Zastosujmy interfejs również dla Lufy!

public interface IStrzelajaceILadujacy
{
	void Strzelaj();
	void LadujPocisk(ISiejacyZniszczenie pociskSiejacyZniszczenie);
}

W klasie Lufa musimy zmienić tylko jedną linijkę.

public class Lufa : IStrzelajaceILadujacy

No to stwórzmy teraz Długa Lufę. Możemy zrobić to w ten sposób:

public class DlugaLufa : IStrzelajaceILadujacy

Nie jest to najlepsze rozwiązanie. Dlaczego? Różnica pomiędzy zwykłą Lufą, a Długa Lufą jest tylko ze względu na wartość jednego pola. W przypadku kiedy DlugaLufa implementuje interface to musimy implementować całą klasę od początku. Więc zróbmy to troszkę inaczej:

public class DlugaLufa : Lufa
{
	public DlugaLufa()
	{
		DlugoscLufy = 250;
		PociskJestZaladowany = false;
	}
}

Oczywiście w klasie Lufa trzeba zmienić właściwości na protected:

public class Lufa : IStrzelajaceILadujacy
{
	protected int DlugoscLufy;
	protected bool PociskJestZaladowany;
	protected ISiejacyZniszczenie Pocisk;
	
	...
}

Zajmijmy się teraz karabinami. Karabin ma tylko jedno zadanie: strzelać. Nauczeni doświadczeniem użyjmy interfejsu. Interfejs IStrzelajaceILadujacy ma już w sobie metodę void Strzelaj(). Więc możemy go użyć.

public class SzybkoszelajcyKarabin : IStrzelajaceILadujacy
{
	public void Strzelaj()
	{
		Console.WriteLine("Pif-Pif-Pif-Pif-Pif-Pif-Pif");
	}

	public void LadujPocisk(ISiejacyZniszczenie pociskSiejacyZniszczenie)
	{
		throw new NotImplementedException();
	}
}

public class CelnyKarain : IStrzelajaceILadujacy
{
	public void Strzelaj()
	{
		Console.WriteLine("Pif-Paf");
	}

	public void LadujPocisk(ISiejacyZniszczenie pociskSiejacyZniszczenie)
	{
		throw new NotImplementedException();
	}
}

Skoro do karabinów nie ładuje się pocisków (pomińmy to że ładuje się amunicje), to niech metody LadujPocisk wyrzucają wyjątek. Te metody nam nie będą potrzebne w karabinach. Do czołgu trzeba dodać pole:

public class Czolg
{
	...
	private readonly IStrzelajaceILadujacy _karabin;

	public Czolg()
	{
		...
		_karabin = new CelnyKarain();
	}
	
        public void StrzelajKarabinem()
        {
            _karabin.Strzelaj();
        }
	...
}

I gotowe! Dzięki wprowadzeniu drugiej zasady SOLID-u możemy dodawać w bardzo łatwy sposób kolejne rodzaje karabinów, luf, napędów bez zmienienia istniejącego kodu. Nasz kod spełnia teraz zasadę:

Program powinien być otwarty na rozszerzenie, ale zamknięte na modyfikacje.

Zobaczmy jak wygląda teraz nasz program:Zasada otwarte zamknięte

Zasada otwarte zamknięte. Zadanie

Chcemy mieć na zmianę: Pancerz Lekki i Pancerz Ciężki? Jak to zaimplementujesz trzymając się zasady OCP?

SOLID – Zasada otwarte zamknięte. Linki.

Przykład OCP nr 1 (ang)
Przykład OCP nr 2 (ang)
Przykład OCP nr 3 (ang)
Trochę więcej teorii na temat OCP

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *