Princip jedné odpovědnosti (SRP - Single Responsibility Principle) je jeden z pěti základních principů metodologie SOLID objektového návrhu. Jedna z jeho nejčastějších definic zní:
Třída by měla mít právě jeden důvod ke změně.
Třída by tedy měla mít pouze jednu odpovědnost - pouze jednu logickou funkcionalitu. Svou definicí patří SRP mezi nejjednodušší principy objektového návrhu, nezkušení objektoví návrháři však tento princip často ve svých programech porušují.
Vysvětleme si problém na příkladě třídy Uzivatel:
public class Uzivatel
{
public String Id { get; set; }
public String Heslo { get; set; }
public void Ulozit(IUloziste uloziste)
{
uloziste.Ulozit<Uzivatel>(this);
}
}
Třída nese informace o uživateli (Id a Heslo) a zajišťuje jeho uložení do úložiště, např. databáze. Zkušenější čtenáři ihned rozpoznali porušení princip jedné odpovědnosti. První odpovědností třídy je reprezentace entity typu uživatel v systému (veřejné atributy Id a Heslo). Druhou odpovědností je uložení stavu objektu do úložiště - metoda Ulozit. Provedeme refaktorizaci do dvou samostatných tříd:
public class Uzivatel
{
public String Id { get; set; }
public String Heslo { get; set; }
}
public class UzivatelDao
{
private IUloziste uloziste;
public UzivatelDao(IUloziste uloziste)
{
this.uloziste = uloziste;
}
public void Ulozit(Uzivatel uzivatel)
{
uloziste.Ulozit<Uzivatel>(uzivatel);
}
}
Ze třídy Uzivatel jsme odstranili metodu Ulozit. Vznikla nová třída UzivatelDao, jejíž odpovědností je zpřístupňovat funkcionalitu související s úložištěm entit Uzivatel, tedy například i uložení.
Pro úplnost zjednodušená definice rozhraní IUloziste:
interface IUloziste
{
void Ulozit<T>(T objekt);
}
Proč je nutné princip dodržovat
- Čitelnost kódu. Někdy dochází k obavám, že rozdělením větší třídy na více menších jednoúčelových tříd se zvýší složitost systému a bude problematické porozumět celku. Opak je pravdou. Navigace ve třídách, které dělají to, co jejich název deklaruje, je mnohem efektivnější. Budete-li procházet velké třídy s více odpovědnostmi, budete zatěžování další funkcionalitou, která Vám bude překážet v nalezení toho, co hledáte. Robert C. Martin přirovnává třídy mající více odpovědností k velké krabici v garáži, ve které máte naházeno všechno různé nářadí. Jednoúčelové třídy naopak působí jako úhledně urovnané malé krabičky obsahující pouze související věci jednoho typu.
- Robustnost. Rozdělením tříd podle odpovědnosti docílíme toho, že případné pozdější požadavky na změnu ovlivní pouze třídu, které by se měly týkat. Např. začnete evidovat nový atribut bool PovolenPristup třídy Uzivatel. Rozšíříte třídu Uzivatel, ale třída UzivatelDao tím nebude nijak zasažena.
- Snížení vzájemné provázanosti tříd. Velké třídy budou potřebovat vazby na více jiných tříd. Např. v našem příkladě problematická třída Uzivatel odkazovala rozhraní IUloziste. Ale entita typu uživatel je přeci v systému na business vrstvě nezávislá na způsobu jeho uložení.
- Testovatelnost. Třída mající pouze jednu odpovědnost je lépe testovatelná. Souvisí to také se snížením provázanosti tříd. Test bude omezen pouze na jednu funkcionalitu. Např. pro otestování třídy Uzivatel nebudete potřebovat instanci IUloziste.
- Soudržnost. Malé třídy mají větší soudržnost, která je v OO návrhu doporučovaná. Více o soudržnosti v některém z dalších příspěvků.
Jak odhalit porušení principu jedné zodpovědnosti
Důležité je samozřejmě získat zkušenosti s objektovým návrhem. Měli byste být také schopni se kriticky ohlédnout za dokončenou prací a zhodnotit dodržování SRP. V případě potřeby třídy refaktorovat.
Jedna poučka říká, že pokud při popisu významu třídy použijete spojku "a", měli byste zbystřit a prověřit dodržování SRP. Podezřelé jsou však i souvětí jiného typu. Pozor na třídy typu "reprezentace auta a výpočet jeho spotřeby", "sestava se zpracováním parametrů", "kreditní karta, která si umí sama strhnout poplatek", apod.
Dejte si pozor na slučování reprezentace entity a funkcionality jejího vytvoření. V těchto případech lze refaktorovat funkcionalitu vzniku instance třídy podle některého z návrhových vzorů pro vytváření objektů.
Další zdroje a ukázky
Dobrý den,nerozumím této části:
OdpovědětVymazatRobustnost - ovlivnění pouze třídy, kt. se týkají, ale UzivatelDAO nebude zasažena.
Copak není při změně vlastností třídy, kt. jsou i v DB, současně upravit i třídu, která ty vlastnosti do DB ukládá?
Jakpak by se to stalo?
(př.)
třída Uživatel - vlastnosti Jméno, Příjmení...
třída ukládající Uživatele -
...
cmd.Parameters.AddWithValue("@jmeno", _jmeno)
cmd.Parameters.AddWithValue("@prijmeni", _prijmeni)
...
a pokud upravím Uživatel, tak je třeba upravit i ukládání..
Vladimír Hála
Děkuji Vladimíre za přečtení a připomínku.
VymazatImplementace UzivatelDao uvedená v příkladu by se při přidání atributu opravdu nemusela měnit. Sama totiž nezná mapování vlastností uživatele na databázové sloupce a pouze volá metodu IUloziste.Ulozit().
IUloziste může být implementováno jako wraper pro NHibernate nebo nějaký jiný persistentní framework. V případě NHibernate je potřeba při inicializaci předat O/R mapovací pravidla (xml nebo Fluent NHibernate). Tzn. přidání atributu změní tuto mapovací definici. Ne však implementaci UzivatelDao.
Může samozřejmě dojít k tomu, že třídu UzivatelDao naimplementujete tak, že bude přímo zapisovat do hodnot nějaké struktury v úložišti. Ale IMHO je to méně přehledné a tím pádem hůře udržovatelné, než dát O/R mapování zvlášť. Pokud navíc využíváte pro O/R mapovací pravidla generování z logického modelu, je oddělení do samostatné třídy (souboru) ještě více vhodné.
Děkuji Vám za odpověď, pokusil jsem se o implementaci, ale nedaří se mi v metodě "Uložit" pracovat s předanou třídou (tr_misto) - její vlastnosti nejsou přístupné.
VymazatVáš příklad jsem převáděl do VB, s C# nemám zkušenost.
Zkoušel jsem zápis snad na tisíc způsobů, ale nedaří se mi ty vlastnosti zpřístupnit.
Mohl byste mi napovědět, kde dělám chybu?
Public Class tr_misto
Private ID As Integer
Public Property _ID() As Integer
Get
Return ID
End Get
Set(ByVal value As Integer)
ID = value
End Set
End Property
Private nazev As String
Public Property _nazev() As String
Get
Return nazev
End Get
Set(ByVal value As String)
nazev = value
End Set
End Property
End Class
Public Class tr_misto_DAO
Implements IUloziste
Private uloziste As IUloziste
Private Shared csDB As String = ConfigurationManager.ConnectionStrings("csDB").ToString
Public Sub New(uloziste As IUloziste)
Me.uloziste = uloziste
End Sub
Public Sub Ulozit(Of tr_misto)(ByVal misto As tr_misto) Implements IUloziste.Ulozit
Dim vysledek As String
Dim conn As SqlConnection = New SqlConnection(csDB)
Dim cmd As SqlCommand = New SqlCommand("spx_misto_INSERT", conn)
Try
With cmd
.CommandType = System.Data.CommandType.StoredProcedure
.Parameters.AddWithValue("@ID", misto._ID)
.Parameters.AddWithValue("@Nazev", misto._nazev)
End With
conn.Open()
vysledek = cmd.ExecuteScalar()
Catch e As Exception
Finally
conn.Close()
End Try
End Sub
Interface IUloziste
Sub Ulozit(Of T)(objekt As T)
End Interface
End Class
Děkuji, Vladimír Hála
Bohužel VisualBasic vůbec neznám :(
VymazatMetoda tr_misto_DAO by mohla mít signaturu:
Public Sub Ulozit(Of T)(ByVal misto As tr_misto)
Pokud to děláte podle příkladu, tak DAO objekt by neměl implementovat IUloziste. Odkaz na tuto instanci dostane při vzniku, tzn. v konstruktoru.
Můj příklad je dost abstraktní a předpokládá, že je inicializován persistentní framework, který umí sám uložit instance příslušných tříd, které jsou mapovány do databáze.
Vážně používáte ve zdrojácích češtinu?
OdpovědětVymazatAno, z kulturně-historicko-personálních důvodů. ;) Dřív mi to tolik nevadilo, ale už jsem v tomto ohledu trochu dospěl a anglicko-česká polévka mně přestala chutnat.
Vymazat