Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: OOP - Design Patterns - Ocena jakości kodu
Forum PHP.pl > Forum > PHP > Object-oriented programming
ziolo
Witam,

Chciałem was prosić o ocenę jakości kodu OOP na przykładzie klasy.
Ostatnio zacząłem zwracać większą uwagę na jakość swojego kodu.

Jest to prosta klasa Mailer. Ma zadanie wysyłać maila oraz zapisywać go do bazy(w razie czego gdyby np była awaria smtp, to dobrze byłoby mieć tego maila w bazie).
Mail to zwykła encja z polami (from, to, subject, body, created) - (kod będzie wykorzystywany w SF2.0)
Pierwsza wersja klasy poniżej, to jest kod jaki zawsze pisałem. Generalnie z braku czasu mógłbym go zostawić. Ale ponieważ chciałem coś poprawić to zacząłem refaktorować.

  1.  
  2. class Mailer
  3. {
  4. /**
  5.   * @var \Swift_Mailer
  6.   */
  7. private $mailer;
  8.  
  9. /**
  10.   * @var EntityManager
  11.   */
  12. private $em;
  13.  
  14.  
  15. function __construct(EntityManager $em, \Swift_Mailer $mailer)
  16. {
  17. $this->em = $em;
  18. $this->mailer = $mailer;
  19. }
  20.  
  21. public function send($from, $to, $subject, $body)
  22. {
  23. $this->sendMail($from, $to, $subject, $body);
  24. $this->addMailToDatabase($from, $to, $subject, $body);
  25. }
  26.  
  27. protected function sendMail($from, $to, $subject, $body)
  28. {
  29. $message = \Swift_Message::newInstance()
  30. ->setSubject("=?UTF-8?B?" . base64_encode($subject) . "?=")
  31. ->setFrom($from)
  32. ->setTo($to)
  33. ->setBody($body, 'text/html');
  34. $this->mailer->send($message);
  35. }
  36.  
  37. protected function addMailToDatabase($from, $to, $subject, $body)
  38. {
  39. $mail = new Mail();
  40. $mail->setFrom($from);
  41. $mail->setBody($body);
  42. $mail->setSubject($subject);
  43. $mail->setTo($to);
  44. $this->em->persist($mail);
  45. $this->em->flush();
  46. }
  47. }


Po pierwsze co mi się nie podoba to jaki pisze Robert C Martin im mniej argumentów mają funkcje tym lepiej.
Chcę mieć publicznie metodę send(from,to,subject,body) dostępną bo tak mi pasuje, ale czy muszę te same argumenty wywoływać w dwóch metodach prywatnych ?
Zastanawiałem się też, czy klasa nie łamię zasadę jednej odpowiedzialności(w końcu ona wysyła maila oraz zapisuje go do bazy) ale uznałem, że nie. Traktuję moją klasę jako Facadę(mamy pierwszy wzorzec projektowy) koordyującą te dwie czynności.

Ok teraz druga wersja kodu:

  1.  
  2. class Mailer
  3. {
  4. /**
  5.   * @var \Swift_Mailer
  6.   */
  7. private $mailer;
  8.  
  9. /**
  10.   * @var EntityManager
  11.   */
  12. private $em;
  13.  
  14.  
  15. function __construct(EntityManager $em, \Swift_Mailer $mailer)
  16. {
  17. $this->em = $em;
  18. $this->mailer = $mailer;
  19. }
  20.  
  21. public function send($from, $to, $subject, $body)
  22. {
  23. $mail = $this->createMail($from, $to, $subject, $body);
  24. $this->sendMail($mail);
  25. $this->save($mail);
  26. }
  27.  
  28. /**
  29.   * @param $from
  30.   * @param $to
  31.   * @param $subject
  32.   * @param $body
  33.   * @return Mail
  34.   */
  35. protected function createMail($from, $to, $subject, $body)
  36. {
  37. $mail = new Mail();
  38. $mail->setFrom($from)
  39. ->setBody($body)
  40. ->setSubject($subject)
  41. ->setTo($to);
  42. return $mail;
  43. }
  44.  
  45. protected function sendMail(Mail $mail)
  46. {
  47. $message = \Swift_Message::newInstance()
  48. ->setSubject("=?UTF-8?B?" . base64_encode($mail->getSubject()) . "?=")
  49. ->setFrom($mail->getFrom())
  50. ->setTo($mail->getTo())
  51. ->setBody($mail->getBody(), 'text/html');
  52. $this->mailer->send($message);
  53. }
  54.  
  55. protected function save(Mail $mail)
  56. {
  57. $this->em->persist($mail);
  58. $this->em->flush();
  59. }
  60. }


Co się zmieniło - dopisaliśmy funkcję createMail - która ma za zadanie, utworzyć instancję klasy Mail i przypisać jej attrybuty. Zmieniłem nazwę metody addMailToDatabase na save. Być może w jakieś klasie dziediczącej będziemy chcieli zapisywać np do pliku a nie do bazy danych. Więc nazwa będzie odpowiedniejsza.
Myślę, że już jest lepiej.
Co możemy zrobić dalej. Na pewno createMail - jest do poprawy(nazwa jest zła, ona nie tylko tworzy ale także buduje mail) pasowałoby wykorzystać wzorzec FactoryMethod, gdybyśmy chcieli zmieniać encję,

  1. class Mailer
  2. {
  3. /**
  4.   * @var \Swift_Mailer
  5.   */
  6. private $mailer;
  7.  
  8. /**
  9.   * @var EntityManager
  10.   */
  11. private $em;
  12.  
  13. /**
  14.   * @var Mail
  15.   */
  16. private $mail;
  17.  
  18.  
  19. function __construct(EntityManager $em, \Swift_Mailer $mailer)
  20. {
  21. $this->em = $em;
  22. $this->mailer = $mailer;
  23. }
  24.  
  25. public function send($from, $to, $subject, $body)
  26. {
  27. $this->createMail();
  28. $this->buildMail($from, $to, $subject, $body);
  29. $this->sendMail();
  30. $this->save();
  31. }
  32.  
  33. protected function createMail()
  34. {
  35. $this->mail = new Mail();
  36. }
  37.  
  38. protected function buildMail($from, $to, $subject, $body)
  39. {
  40. $this->mail->setFrom($from)
  41. ->setBody($body)
  42. ->setSubject($subject)
  43. ->setTo($to);
  44. }
  45.  
  46. protected function sendMail()
  47. {
  48. $message = \Swift_Message::newInstance()
  49. ->setSubject("=?UTF-8?B?" . base64_encode($this->mail->getSubject()) . "?=")
  50. ->setFrom($this->mail->getFrom())
  51. ->setTo($this->mail->getTo())
  52. ->setBody($this->mail->getBody(), 'text/html');
  53. $this->mailer->send($message);
  54. }
  55.  
  56. protected function save()
  57. {
  58. $this->em->persist($this->mail);
  59. $this->em->flush();
  60. }
  61. }


Ok co zrobiliśmy - mamy teraz dwie metody createMail oraz buildMail, naturalnym okazało się przeniesienie $mail jako atrybut klasy, dzięki temu w poszeczególnych metodach mamy mniej argumentów - super.
Czy to już koniec ? Moim zdaniem nie, zdecydowanie pasuję zrobić MailerBuilder(kolejny wzorzec) osobną klasę z metodami createMail oraz buildMail.


Ok więc teraz wersja 4 kodu:

  1.  
  2.  
  3. class MailBuilder
  4. {
  5. /**
  6.   * @var Mail
  7.   */
  8. private $mail;
  9.  
  10. public function createMail()
  11. {
  12. $this->mail = new Mail();
  13. return $this;
  14. }
  15.  
  16. public function buildMail($from, $to, $subject, $body)
  17. {
  18. $this->mail->setFrom($from)
  19. ->setBody($body)
  20. ->setSubject($subject)
  21. ->setTo($to);
  22. return $this;
  23. }
  24.  
  25. public function getMail()
  26. {
  27. return $this->mail;
  28. }
  29. }


  1. use Doctrine\ORM\EntityManager;
  2.  
  3. class Mailer
  4. {
  5. /**
  6.   * @var \Swift_Mailer
  7.   */
  8. private $mailer;
  9.  
  10. /**
  11.   * @var EntityManager
  12.   */
  13. private $em;
  14.  
  15. /**
  16.   * @var Mail
  17.   */
  18. private $mail;
  19.  
  20. /**
  21.   * @var MailBuilder
  22.   */
  23. private $builder;
  24.  
  25.  
  26. function __construct(EntityManager $em, \Swift_Mailer $mailer, MailBuilder $builder)
  27. {
  28. $this->em = $em;
  29. $this->mailer = $mailer;
  30. $this->builder = $builder;
  31. }
  32.  
  33. public function send($from, $to, $subject, $body)
  34. {
  35. $this->mail = $this->builder
  36. ->createMail()
  37. ->buildMail($from, $to, $subject, $body)
  38. ->getMail();
  39. $this->sendMail();
  40. $this->save();
  41. }
  42.  
  43. protected function sendMail()
  44. {
  45. $message = \Swift_Message::newInstance()
  46. ->setSubject("=?UTF-8?B?" . base64_encode($this->mail->getSubject()) . "?=")
  47. ->setFrom($this->mail->getFrom())
  48. ->setTo($this->mail->getTo())
  49. ->setBody($this->mail->getBody(), 'text/html');
  50. $this->mailer->send($message);
  51. }
  52.  
  53. protected function save()
  54. {
  55. $this->em->persist($this->mail);
  56. $this->em->flush();
  57. }
  58. }


Ok mamy w końcu zamiast jednej klasy, dwie klasy. MailerBuilder i Mailer. W Mailerze w fajny sposób wykorzystujemy Buildera.
Wszystko ok, generalnie mógłbym zakończyć refaktoryzacje. Niby wszystko ok, możemy przeciążąć save, sendMail, Buildera.
Ale ciągle coś mi tu nie pasuję, czy to łamanie zasady jednej odpowiedzialności, czy może to, że w Mailer wykorzystujemy EntityManager oraz SwiftMailer a nie powinniśmy.
No właśnie zdecydowanie należy stworzyć kolejne abstrakcje nazwijmy Sender or Saver i wykorzystać wzorzec Strategy.

Ok a więc teraz wersja 5 kodu. Namnożyło się klas i interfejsów smile.gif

  1. interface SaverInterface
  2. {
  3. public function save(Mail $mail);
  4. }


  1. class Saver implements SaverInterface
  2. {
  3. /**
  4.   * @var EntityManager
  5.   */
  6. private $em;
  7.  
  8. function __construct(EntityManager $em)
  9. {
  10. $this->em = $em;
  11. }
  12.  
  13. public function save(Mail $mail)
  14. {
  15. $this->em->persist($mail);
  16. $this->em->flush();
  17. }
  18. }


SaverInterface oraz implementacja Savera zapisująca mail do bazy.

  1. interface SenderInterface
  2. {
  3. public function sendMail(Mail $mail);
  4. }


  1. class Sender implements SenderInterface
  2. {
  3. /**
  4.   * @var \Swift_Mailer
  5.   */
  6. private $mailer;
  7.  
  8. function __construct(\Swift_Mailer $mailer)
  9. {
  10. $this->mailer = $mailer;
  11. }
  12.  
  13. public function sendMail(Mail $mail)
  14. {
  15. $message = \Swift_Message::newInstance()
  16. ->setSubject("=?UTF-8?B?" . base64_encode($mail->getSubject()) . "?=")
  17. ->setFrom($mail->getFrom())
  18. ->setTo($mail->getTo())
  19. ->setBody($mail->getBody(), 'text/html');
  20. $this->mailer->send($message);
  21. }
  22. }


SenderInteface oraz implementacja Sendera wykorzystująca Swift_Mailer.

  1. class Mailer
  2. {
  3. /**
  4.   * @var SenderInterface
  5.   */
  6. private $sender;
  7.  
  8. /**
  9.   * @var SaverInterface
  10.   */
  11. private $saver;
  12.  
  13. /**
  14.   * @var MailBuilder
  15.   */
  16. private $builder;
  17.  
  18.  
  19. function __construct(SenderInterface $sender, SaverInterface $saver, MailBuilder $builder)
  20. {
  21. $this->sender = $sender;
  22. $this->saver = $saver;
  23. $this->builder = $builder;
  24. }
  25.  
  26. public function send($from, $to, $subject, $body)
  27. {
  28. $mail = $this->builder
  29. ->createMail()
  30. ->buildMail($from, $to, $subject, $body)
  31. ->getMail();
  32. $this->sender->sendMail($mail);
  33. $this->saver->save($mail);
  34. }
  35. }


Oraz nasza klasa Mailera, która nie potrzebuje już zależności EntityManager oraz Swift_Mailer. Klasa MailBuilder nie zmieniła się już w stosunku do wersji 4.

Teraz mamy chyba wszystko idealnie, możemy sobie konfigurować Mailera dowolnie z różnych Builderów, Senderów or Saverów. Pozbyłem się też wrażenia, że Mailer łamie zasadę jednej odpowiedzialności, teraz wyraźnie widać, że on koordynuję pracę róznych obiektów. Wykorzystane wzorce projektowe(Facade, Builder, FactoryMethod, Strategy, DI)

I teraz chciałem się zapytać o ocenę, jakbyście ocenieli wersję 5 kodu, w skali 1-10.
Patrząc też na to jak się zmieniłą wersja 1 od wersji5. Kto uważa, że to jest przerost formy nad treścią ?
skowron-line
Dam 2 za chęci.
Klasa Saver to pomyłka
1. wstrzykujesz EntityManager a jak bedzie inny manager to sie wywali (design by contract)
2. metoda Save tez przyjmuje tylko klasy Mail, czyli dla klasy Person bedzie inny saver, hmmm słabo


Piszesz klase do wysylki maili w ktorej odwolujesz sie do klasy do wysylki maili, tongue.gif troche słabe.

No i najwazniejsze SRP (SOLID) zapis to zapis a wysylka to wysylka nie wpychaj tego do jednej klasy, zrob sobie tak ze zapisujesz do bazy maile a pozniej odczytujesz i wysylasz.
ziolo
@skowron-line Dzięki wielkie za odpowiedź.

Cytat(skowron-line @ 5.10.2014, 15:06:40 ) *
Dam 2 za chęci.
Klasa Saver to pomyłka
1. wstrzykujesz EntityManager a jak bedzie inny manager to sie wywali (design by contract)
2. metoda Save tez przyjmuje tylko klasy Mail, czyli dla klasy Person bedzie inny saver, hmmm słabo


1) nie bardzo rozumiem o co Ci chodzi jaki inny EntityManager. Może nie być żadnego EntityManagera, co on ma wspólnego z wysypaniem się.
Mogę sobię stworzyć SaverToCsv implementujący metodę save - zapisujący po prostu maile do pliku csv.
Do klasy Mailer wstrzykuje tylko obiekt typu SaverInterface - mający metodę save. To mnie interesuje w klasie Mailer, a jak to już robi Saver to jego sprawa.

2) Mail - to jest encja własna tego bundla i Mailer tylko ją będzie wykorzystywał, nie będzie żadnych Person. Ok też właściwie powinno być MailInterface -(z getterami i stterami) ale to sobie już darowałem.

Cytat(skowron-line @ 5.10.2014, 15:06:40 ) *
Piszesz klase do wysylki maili w ktorej odwolujesz sie do klasy do wysylki maili, tongue.gif troche słabe.


Chodzi o klasę Sender ?
Ok z tym się częściowo zgodzę, ale załóżmy, że np chciałbym wysyłać np wiadomości na facebooka albo sms, tak sobie wstrzykuje różnych senderów. Mailer mi się nie zmienia i o to chodzi.
Inna sprawa, że pasowałoby nazwać klasę zamiast Mailer(np Messager, Notifier)


Cytat(skowron-line @ 5.10.2014, 15:06:40 ) *
No i najwazniejsze SRP (SOLID) zapis to zapis a wysylka to wysylka nie wpychaj tego do jednej klasy, zrob sobie tak ze zapisujesz do bazy maile a pozniej odczytujesz i wysylasz.


Idęą tej klasy był łatwy interfejs do wysyłania maili i przy okazji zapisywanie ich gdzieś.
Chcę sobie w kontrolerze mieć prosty zapis:
  1. $this->get('own_mailer')->send($from,$to,$subject,$body)


Wysyłaniem zapisywaniem zajmują się: Sender i Saver. Mogę sobię stworzyć wiele instancji klas Mailer z różnymi konfiguracjami teraz.
Klasa Mailer nie łamie zasady SRP. Mailer ma jedną odpowiedzialność skoordynować te operacje udostępniając klientowi prosty interfejs - czytaj wzorzec Facade.
Druga opcja dla mnie odpada, to ma być prosty bundle, wykorzystywany w wielu projektach, nawet mniejszych, wszędzie trzeba by pamiętać np skonfigurować crona do wysyłki.
irmidjusz
Spoko rozwiązanie, ale co, jeśli zechcę zapisać maila za pomocą kilku różnych implementacji SaverInterface jednocześnie? tongue.gif
ziolo
Cytat(irmidjusz @ 5.10.2014, 21:20:09 ) *
Spoko rozwiązanie, ale co, jeśli zechcę zapisać maila za pomocą kilku różnych implementacji SaverInterface jednocześnie? tongue.gif


Faktycznie dobry pomysł smile.gif
Teraz jest jeszcze elastyczniej, tak samo można wstrzykiwać tablicę senderów jakbyśmy chcieli.
Wychodzi też zaleta OOP, bo kolejne wymagania/zmiany wymagań dużo łatwiej zakodować tongue.gif

  1. class Mailer
  2. {
  3. /**
  4.   * @var SenderInterface[]
  5.   */
  6. private $senders;
  7.  
  8. /**
  9.   * @var SaverInterface[]
  10.   */
  11. private $savers;
  12.  
  13. /**
  14.   * @var MailBuilder
  15.   */
  16. private $builder;
  17.  
  18.  
  19. function __construct(SenderInterface[] $senders, SaverInterface[] $savers, MailBuilder $builder)
  20. {
  21. $this->senders = $senders;
  22. $this->savers = $savers;
  23. $this->builder = $builder;
  24. }
  25.  
  26. public function send($from, $to, $subject, $body)
  27. {
  28. $mail = $this->builder
  29. ->createMail()
  30. ->buildMail($from, $to, $subject, $body)
  31. ->getMail();
  32. foreach($this->senders as $sender){
  33. $sender->sendMail($mail);
  34. }
  35. foreach($this->savers as $saver){
  36. $saver->save($mail);
  37. }
  38. }
  39. }
MateuszS
Hm akurat w tak prostym mailerze raczej ciężko o jakieś zaawansowane obiektowe rozwiązania.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2024 Invision Power Services, Inc.