SOLID – część 1. Zasada Jednej Odpowiedzialności

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 Zasada Jednej Odpowiedzialności.

Naszym wdzięcznym przykładem niech będzie czołg.

Czołg może jeździć, strzelać i ładować pociski do wystrzelenia. Czołg ma gąsienice. Dzięki swojemu napędowi może rozwinąć pewną maksymalną prędkość. Jak każdy szanujący się pojazd bojowy ma pancerz o odpowiedniej grubości. Lufa ma swoją długość. Do lufy można załadować pocisk i z niej go wystrzelić. Lufa ma właściwość, która przechowuje informacje o tym, czy ma już załadowany pocisk. Pocisk ma właściwość prawdopodobieństwa trafienia w cel. Dodatkowo pocisk przechowuje takie informacje jak: czy został zużyty i czy zniszczył cel. Pocisk może zniszczyć cel.

Napiszmy więc prostą klasę czołgu:

public class Czolg
{
	private readonly double _trafnoscPocisku;        
	private readonly int _gruboscPancerza;
	private readonly int _dlugoscLufy;
	private readonly int _predkoscMaksynalna;
	private bool _pociskJestZaladowany;
	private bool _pociskaZostalZuzyty;
	private bool _pociskZniszczylCel;

	public Czolg()
	{
		_predkoscMaksynalna = 60;
		_dlugoscLufy = 120;
		_gruboscPancerza = 30;
		_trafnoscPocisku = 0.5;
		_pociskJestZaladowany = false;
		_pociskZniszczylCel = false;
		_pociskaZostalZuzyty = false;
	}

	public string Jedz()
	{
		return "Przechajales krotka odleglosc. Ale wjechales pod gorke.";
	}

        public void Strzelaj()
        {
            if (_pociskJestZaladowany)
            {
                _pociskJestZaladowany = false;
                Zniszcz();
            }
            else
            {
                throw new PociskNieJestZaladowany();
            }
        }

	public void LadujPocisk()
	{
		if (!_pociskJestZaladowany)
		{
			_pociskJestZaladowany = true;
		}
		else
		{
			throw new PociskJestJuzZaladowany();
		}
	}

	private void Zniszcz()
	{
		if (!SprawdzCzyPociskZostalZuzyty())
		{
			_pociskaZostalZuzyty = true;

			if (SprawdzCzyPociskZniszczylCel())
				_pociskZniszczylCel = true;
			else
				_pociskZniszczylCel = false;
		}
		else
		{
			throw new PociskZostalJuzZuzyty();
		}
	}

	private bool SprawdzCzyPociskZostalZuzyty()
	{
		return _pociskaZostalZuzyty == false;
	}

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

public class PociskNieZostalZuzyty : Exception
{

}

public class PociskZostalJuzZuzyty : Exception
{

}

public class PociskJestJuzZaladowany : Exception
{

}

public class PociskNieJestZaladowany : Exception
{

}

Aplikujemy SOLID

S – Single Responsiblity Princlipe (Zasada Jednej Odpowiedzialności)
Wyobraźmy sobie, że zastajemy taki program. Ty i Twój kolega dostajecie 2 zadania. Trzeba dodać wagę pocisku oraz trzeba dodać siłę wyrzutu jaką dysponuje lufa. Zadania rozdzielacie po równo. Ty bohatersko mówisz, że dodasz wagę pocisku, a koledze zostawiasz możliwość dodania siły wyrzutu z lufy. Zabieracie się do pracy. Obydwoje dodajcie linijki do klasy Czołg. Commit. Push. Jedna osoba musi mergować.

Co mówi Zasada Jednej Odpowiedzialności, czyli pierwsza zasad SOLID?

Nigdy nie powinno być więcej niż jednego powodu do modyfikacji klasy.

Ty i Twój kolega dostaliście dwa różne zadania. A edytujecie tą samą klasę. Łamiecie pierwszą zasadę SOLID. To sugeruje, że klasa czołg ma za dużo odpowiedzialności. Zadania dotyczą całkowicie dwóch różnych części czołgu. Te dwie różne rzeczy powinny być rozdzielone do dwóch różnych klas. To zapewniłoby łatwość edytowania kodu (i unikania zbędnego mergowania). Ale jak odpowiednio podzielić kod na klasy?

Podczas studiowania na jednych z zajęć zajmowaliśmy się tworzeniem modeli UMLowych. Musieliśmy na podstawie tekstu stworzyć klasy, opisać ich metody oraz właściwości. Zasady były proste. Rzeczownik to klasa. Przymiotnik lub coś co opisuje stan klasy są właściwościami klasy. Czasowniki są metodami klas. Agregacje (czy też kompozycje) to połączenia pomiędzy klasami (które w kodzie będą reprezentowane przez osobne pola).

Idąc tym tropem możemy uzyskać następujący diagram klas dla tekstu, który opisuje czołg.
Zasada Jednej Odpowiedzialności

Czołg kontroluje Lufę i Gąsienice. Więc klasa czołg dodatkowo będzie miała 3 metody: Strzelaj(), ŁadujPocisk(Pocisk pocisk) oraz Jedz(), które będą aktywować metody w Lufie i Gąsienicy. Taki prosty zabieg pozwala podzielić odpowiedzialność pomiędzy klasami.

public class Czolg
{
	private Pancerz _pancerz;
	private Lufa _lufa;
	private Gasienice _gasienice;

	public Czolg()
	{
		_pancerz = new Pancerz();
		_lufa = new Lufa();
		_gasienice = new Gasienice();
	}

	public void Jedz()
	{
		_gasienice.Jedz();
	}

	public void Strzelaj()
	{
		_lufa.Strzelaj();
	}

	public void LadujPociks(Pocisk pocisk)
	{
		_lufa.LadujPocisk(pocisk);
	}
}
public class Gasienice
{
	private int _predkoscMaksynalna;

	public Gasienice()
	{
		_predkoscMaksynalna = 60;
	}

	public void Jedz()
	{
		Console.WriteLine("Przechajales krotka odleglosc. Ale wjechales pod gorke.");
	}
}
public class Pancerz
{
	private int _gruboscPancerza;

	public Pancerz()
	{
		_gruboscPancerza = 30;
	}
}
public class Lufa 
{
	private int _dlugoscLufy;
	private bool _pociskJestZaladowany;
	private Pocisk _pocisk;

	public Lufa()
	{
		_dlugoscLufy = 120;
		_pociskJestZaladowany = false;
	}

        public void Strzelaj()
        {
            if (_pociskJestZaladowany)
            {
                _pociskJestZaladowany = false;
                Zniszcz();
            }
            else
            {
                throw new PociskNieJestZaladowany();
            }
        }

	public void LadujPocisk(Pocisk pocisk)
	{
		if (!_pociskJestZaladowany)
		{
			_pociskJestZaladowany = true;
			_pocisk = pocisk;
		}
		else
		{
			throw new PociskJestJuzZaladowany();
		}
	}
}

public class PociskJestJuzZaladowany : Exception
{

}

public class PociskNieJestZaladowany : Exception
{

}
public class Pocisk
{
	private double _trafnoscPocisku;
	private bool _pociskaZostalZuzyty;
	private bool _pociskZniszczylCel;

	public Pocisk()
	{
		_trafnoscPocisku = 0.5;
		_pociskZniszczylCel = false;
		_pociskaZostalZuzyty = false;
	}

	public void Zniszcz()
	{
		if (!SprawdzCzyPociskZostalZuzyty())
		{
			_pociskaZostalZuzyty = true;

			if (SprawdzCzyPociskZniszczylCel())
				_pociskZniszczylCel = true;
			else
				_pociskZniszczylCel = false;
		}
		else
		{
			throw new PociskZostalJuzZuzyty();
		}
	}

	private bool SprawdzCzyPociskZostalZuzyty()
	{
		return _pociskaZostalZuzyty == false;
	}

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

public class PociskNieZostalZuzyty : Exception
{

}

public class PociskZostalJuzZuzyty : Exception
{

}

W tym momencie jeżeli przyszłoby Tobie i Twojemu znajomemu jeszcze raz podjeść do zadania dodania właściwości do Lufy i Pocisku to każdy miałby swoja klasę w której mógłby wprowadzić odpowiednie zmiany. Przypomnijmy sobie co mówi Zasada Jednej Odpowiedzialności:

Nigdy nie powinno być więcej niż jednego powodu do modyfikacji klasy.

Nasz kod spełnia tę zasadę.

SOLID – Zasada jednej odpowiedzialności. Linki.

Przykład SRP nr 1 (ang)
Przykład SRP nr 2 (ang)
Kiedy poznać, że osiągnęliśmy SRP? (ang)
Podpowiedzi co może być osobą klasą (ang)

2 thoughts on “SOLID – część 1. Zasada Jednej Odpowiedzialności”

  1. No i teraz znowu kolega a chce zmienic sposob obslugi wlasciwoscu dlugosc lufy natomiast kolega b zasade obslugi czy wystrzelony, dalej musza pracowac na jednej klasie dalej sa dwa powody do zmiany, tak mozna w nieskonczonosc, paranoja:)?

  2. Coś pięknego. Genialny przykład i krok po kroku tłumaczenie. Dzięki Tobie poznawanie SOLID staję się proste.

Leave a Comment

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