S v SOLIDe
Milan Herda, 03 / 2018
Kto som
programátor (PHP, JavaScript), hráč spoločenských hier, fanúšik sci-fi, wannabe autor browser hier (feudarium.com)
Profesia, FatChilli, Porada, BlueOrange, NOV
profesia.sk, domelia.sk, rtvs.sk, reality.sme.sk, living.sk...
O čom budeme hovoriť
- Čo je SOLID
- Single Responsibility Principle
- Výhody Single Responsibility
- Ako rozpoznať porušenia princípu
- Ako refaktorovať, aby sme dodržali princíp
SOLID
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Skratka predstavujúca 5 základných princípov dobrého softvérového návrhu
Single Responsibility Principle
Trieda by mala mať jeden (a iba jeden) dôvod pre svoju zmenu.
Výhody dodržiavania
Single Responsibility Principle
- jednoduchšie úpravy (ľahkou zmenou kódu alebo výmenou celej triedy)
- bezpečnejšie úpravy (lebo v kóde nie sú nesúvisiace veci)
- jednoduchšie pochopenie toho, čo trieda robí
- ľahká testovateľnosť
Symptómy porušenia princípu
- trieda má veľa properties
- trieda má veľa public metód
- každá metóda používa iné properties
- špecifické úlohy sú delegované na privátne metódy
Ako refaktorovať
- extrahovať špecifické úlohy do privátnych metód
- identifikovať skrytých kolaborantov
- preniesť kód skrytých kolaborantov do samostatných tried
class ConfirmationMailer {
private TemplatingInterface $templating;
private TranslatorInterface $translator;
private MailerInterface $mailer;
public function __construct(TemplatingInterface $templating, TranslatorInterface $translator, MailerInterface $mailer) {
$this->templating = $templating;
$this->translator = $translator;
$this->mailer = $mailer;
}
public function sendToUser(User $user) {
$subject = $this->translator->translate('Confirm your email address');
$body = $this->templating->render(
'confirmationEmail.tpl',
[
'user' => $user,
]
);
$message = new Message($subject, $body);
$message->setTo($user->getEmailAddress());
$this->mailer->send($message);
}
}
Táto trieda má dve zodpovednosti a teda aj dva dôvody pre zmenu
- odoslanie emailu
- vytvorenie emailu
Refactoring time!
Skúsime triedu zrefaktorovať
Stiahnite si zdrojáky
Krok 1: extrahovať špecifické úlohy do privátnych metód
public function sendToUser(User $user) {
$message = $this->createMessageForUser($user);
$this->sendMessage($message);
}
private function createMessageForUser(User $user) {
$subject = $this->translator->translate('Confirm your email address');
$body = $this->templating->render(
'confirmationEmail.tpl',
[
'user' => $user,
]
);
$message = new Message($subject, $body);
$message->setTo($user->getEmailAddress());
return $message;
}
private function sendMessage(MessageInterface $message) {
$this->mailer->send($message);
}
- odoslanie emailu
- vytvorenie emailu
Krok 2: identifikovať skrytých kolaborantov
Pozrieme sa na nové privátne metódy a rozhodneme sa, ktorá z nich patrí triede a ostane.
Zamyslíme sa, aká je zodpovednosť zvyšných metód a do akých tried patria.
V našom prípade patrí createMessageForUser do inej triedy
Krok 3: presun kolaborantov do samostatných tried
Názov metódy nám hovorí, že sa niečo vytvára. Takže nová trieda bude továrničkou (factory)
createMessageForUser presunieme do samostatnej triedy
Výsledok: Factory
class ConfirmationMessageFactory
{
private TemplatingInterface $templating;
private TranslatorInterface $translator;
public function __construct(TemplatingInterface $templating, TranslatorInterface $translator)
{
$this->templating = $templating;
$this->translator = $translator;
}
public function createMessageForUser(User $user): MessageInterface
{
$subject = $this->translator->translate('Confirm your email address');
$body = $this->templating->render(
'confirmationEmail.tpl',
[
'user' => $user,
]
);
$message = new Message($subject, $body);
$message->setTo($user->getEmailAddress());
return $message;
}
}
Výsledok: Mailer
class ConfirmationMailer
{
private ConfirmationMessageFactory $confirmationMessageFactory;
private MailerInterface $mailer;
public function __construct(
ConfirmationMessageFactory $confirmationMessageFactory,
MailerInterface $mailer
) {
$this->confirmationMessageFactory = $confirmationMessageFactory;
$this->mailer = $mailer;
}
public function sendToUser(User $user)
{
$message = $this->confirmationMessageFactory->createMessageForUser($user);
$this->mailer->send($message);
}
}
Príklad 2
class Building
{
private int $id;
private int $type;
private string $name;
private int $level;
private TownInterface $town;
private array $priceTable;
private DatabaseConnectionInterface $databaseConnection;
public function __construct(DatabaseConnectionInterface $databaseConnection, array $priceTable) { /* ... */ }
public function setId($id) { /* ... */ }
public function getId() { /* ... */ }
public function setType($type) { /* ... */ }
public function getType() { /* ... */ }
public function setName($name) { /* ... */ }
public function getName() { /* ... */ }
public function setLevel($level) { /* ... */ }
public function getLevel() { /* ... */ }
public function setTown(TownInterface $town) { /* ... */ }
public function getTown() { /* ... */ }
public function getPrice($level = 0) { /* ... */ }
public function build() { /* ... */ }
public function upgrade() { /* ... */ }
}
Aké sú zodpovednosti triedy?
- uchovávať informácie o budove (id, typ, level...)
- poskytovať informáciu o cene budovy pre rôzny level
- postavenie budovy
- upgrade budovy
Krok 1: extrahovať špecifické úlohy do privátnych metód
Nemusíme robiť, trieda je celkom pekne rozdelená
Krok 2: identifikovať skrytých kolaborantov
- informácie o budove necháme v triede Building
- tabuľka cien by mala byť samostatným objektom
- práca s databázou je zodpovednosťou inej triedy (repozitár)
- prípadne podľa použitia môžeme zvážiť vytvorenie tried zabezpečujúcich stavbu budov
Krok 3: presun kolaborantov do samostatných tried
Vytvoríme nové triedy
- tabuľku cien budovy nazvanú napr. BuildingPriceTable
- Repozitár pre databázové operácie k budove, BuildingRepository
Výsledok: BuildingPriceTable
class BuildingPriceTable
{
/** @var array<int,ResourcesInterface> */
private array $pricesPerLevel;
public function __construct(array $pricesPerLevel)
{
$this->pricesPerLevel = $pricesPerLevel;
}
public function getPrice(int $level): ResourcesInterface
{
if (array_key_exists($level, $this->priceTable)) {
return $this->priceTable[$level];
}
throw new InvalidLevelException();
}
}
Výsledok: BuildingRepository
class BuildingRepository
{
private DatabaseConnectionInterface $databaseConnection;
public function __construct(DatabaseConnectionInterface $databaseConnection)
{
$this->databaseConnection = $databaseConnection;
}
public function buildNewBuilding(int $type, int $townId): int
{
$id = $this->databaseConnection->insert(
'building',
[
'type%i' => $type,
'level%i' => 1,
'town_id%i' => $townId,
]
);
return $id;
}
public function upgradeBuilding(int $buildingId)
{
$this->databaseConnection
->update(
'building',
[
'level%sql' => 'level + 1',
]
)
->where('id = %i', $buildingId);
}
}
Výsledok: Building
class Building
{
private int $id;
private int $type;
private string $name;
private int $level;
private TownInterface $town;
public function setId($id) { /* ... */ }
public function getId() { /* ... */ }
public function setType($type) { /* ... */ }
public function getType() { /* ... */ }
public function setName($name) { /* ... */ }
public function getName() { /* ... */ }
public function setLevel($level) { /* ... */ }
public function getLevel() { /* ... */ }
public function setTown(TownInterface $town) { /* ... */ }
public function getTown() { /* ... */ }
}
Príklad 3
public function editArticleAction(
int $articleId,
RequestInterface $request
): ResponseInterface {
$query = $this->databaseConnection
->select('id, title, text')
->from('article')
->where('id = %i', $articleId);
$result = $query->execute();
$row = $result->fetchRow();
$article = null;
if ($row) {
$article = new Article();
$article->setId($row['id'])
->setTitle($row['title'])
->setText($row['text']);
}
if (!$article) {
throw new Status404NotFoundException();
}
if ($request->isMethod('POST')) {
$title = $request->getParam('title', '');
$text = $request->getParam('text', '');
if (
(trim($title) !== '')
&& (trim($text) !== '')
) {
$article->setTitle($title)
->setBody($body);
$this->databaseConnection
->update(
'article',
[
'title%s' => $title,
'text%s' => $text,
]
)
->where('id = %i', $articleId)
->execute();
if (!array_key_exists('flash_messages', $_SESSION)) {
$_SESSION['flash_messages'] = [];
}
if (!isset($_SESSION['flash_messages']['info'])) {
$_SESSION['flash_messages']['info'] = [];
}
$_SESSION['flash_messages']['info'][] =
$this->translator->trans('Article was updated');
$detailUrl = '/article/' . $article->getTitle();
return new RedirectResponse($detailUrl);
}
}
$html = $this->templating->render(
'article/edit.html.twig',
[
'article' => $article,
]
);
return new Response($html);
}
Aké sú zodpovednosti triedy?
- vyhľadať v databáze článok podľa ID
- vytvoriť inštanciu triedy Article
- naplniť článok hodnotami z odoslaného formuláru
- upraviť článok v databáze
- zapísať flash správu
- urobiť redirect
- vyrendrovať stránku s editačným formulárom
Postup refaktoringu
Postup je rovnaký, ale príliš dlhý pre slajdy :)
Urobíme to spoločne
Opakovanie
Trieda by mala mať jeden (a iba jeden) dôvod pre svoju zmenu.
Symptómy porušenia princípu
- trieda má veľa properties
- trieda má veľa public metód
- každá metóda používa iné properties
- špecifické úlohy sú delegované na privátne metódy
Námietka voči Single Responsibility Principle
SRP nás núti vytvárať priveľa tried a tým sa stráca prehľadnosť
- áno, vzniká viacej tried
- sú však malé a ich zodpovednosti jasné
- keď sú dobre pomenované, netreba študovať ich kód, čiže prehľadnosť stúpa
- sú znovupoužiteľné
Viac informácií:
Matthias Noback - Principles of Package Design
Ďakujem za pozornosť
S v SOLIDe
By Milan Herda
S v SOLIDe
- 679