Независимо от това, което смятаме за страхотен код, той винаги изисква едно просто качество: кодът трябва да бъде поддържан. Правилното отстъпване, чисти имена на променливи, 100% покритие на теста и така нататък може да ви отведе само дотук. Всеки код, който не може да се поддържа и не може да се адаптира към променящите се изисквания с относителна лекота, е код, който само чака да остарее. Може да не се наложи да напишем страхотен код, когато се опитваме да изградим прототип, доказателство за концепция или минимално жизнеспособен продукт, но във всички останали случаи винаги трябва да пишем код, който може да се поддържа. Това е нещо, което трябва да се счита за основно качество на софтуерното инженерство и дизайн.
В тази статия ще обсъдя как Принципът на единна отговорност и някои техники, които се въртят около него, могат да дадат на вашия код точно това качество. Писането на страхотен код е изкуство, но някои принципи винаги могат да помогнат на вашата разработка да насочи посоката, към която трябва да се насочи, за да създаде здрав и поддържаем софтуер.
комуникацията става по-лесна, когато увеличите броя на членовете на екипа
Почти всяка книга за някаква нова MVC (MVP, MVVM или друга M **) рамка е пълна с примери за лош код. Тези примери се опитват да покажат какво предлага рамката. Но в крайна сметка те също предоставят лоши съвети за начинаещи. Примери като „да кажем, че имаме този ORM X за нашите модели, шаблониращ двигател Y за нашите възгледи и ще имаме контролери, които да управляват всичко това“ постигат нищо друго освен огромни контролери.
Въпреки че в защита на тези книги, примерите имат за цел да демонстрират лекотата, с която можете да започнете с тяхната рамка. Те не са предназначени да преподават софтуерен дизайн. Но читателите, следващи тези примери, осъзнават едва след години колко контрапродуктивно е да има монолитни парчета код в своя проект.
Моделите са сърцето на вашето приложение. Ако имате модели, отделени от останалата част от логиката на вашето приложение, поддръжката ще бъде много по-лесна, независимо колко сложно става приложението ви. Дори и за сложни приложения, доброто внедряване на модел може да доведе до изключително изразителен код. И за да постигнете това, започнете, като се уверите, че вашите модели правят само това, което им е писано, и не се занимавайте с това, което приложението, изградено около него, прави. Освен това, той не се занимава с това какъв е основният слой за съхранение на данни: приложението ви разчита ли на база данни на SQL или съхранява всичко в текстови файлове?
Докато продължаваме тази статия, ще разберете колко страхотен код е много за разделянето на загрижеността.
Вероятно сте чували за принципите на SOLID: единична отговорност, отворено-затворено, заместване на liskov, сегрегация на интерфейса и инверсия на зависимост. Първата буква, S, представлява Принцип на единна отговорност (SRP) и неговото значение не може да бъде надценено. Дори бих твърдял, че това е необходимо и достатъчно условие за добър код. Всъщност във всеки код, който е написан лошо, винаги можете да намерите клас, който носи повече от една отговорност - form1.cs или index.php, съдържащ няколко хиляди реда код, не е нещо, което се среща рядко и всички ние вероятно са го виждали или правили.
Нека да разгледаме пример в C # (ASP.NET MVC и Entity framework). Дори и да не сте Разработчик на C # , с малко опит с ООП, ще можете лесно да го следвате.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction('Index'); } ... (many more methods like Create here) }
Това е обикновен клас OrderController, показан е неговият метод Създаване. В контролери като този често виждам случаи, когато самият клас Order се използва като параметър на заявка. Но предпочитам да използвам специални класове заявки. Отново, SRP!
Забележете във фрагмента на кода по-горе, че контролерът знае твърде много за „поставяне на поръчка“, включително, но не само, съхраняване на обекта „Поръчка“, изпращане на имейли и др. За всяка малка промяна разработчикът трябва да промени целия код на контролера. И само в случай, че друг контролер също трябва да създава поръчки, по-често разработчиците ще прибегнат до копиране-поставяне на кода. Контролерите трябва да контролират само цялостния процес, а не всъщност да съхраняват всяка част от логиката на процеса.
c/c++
Но днес е денят, в който спираме да пишем тези огромни контролери!
Нека първо извлечем цялата бизнес логика от контролера и я преместим в клас OrderService:
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction('Index'); }
С това, контролерът сега прави само това, което е предназначено: да контролира процеса. Той знае само за изгледите, класовете OrderService и OrderRequest - най-малкото набор от информация, необходима му, за да си върши работата, която е управление на заявки и изпращане на отговори.
По този начин рядко ще променяте кода на контролера. Други компоненти като изгледи, обекти на заявки и услуги все още могат да се променят, тъй като са свързани с бизнес изисквания, но не и контролери.
За това е SRP и има много техники за писане на код, които отговарят на този принцип. Един пример за това е инжектирането на зависимост (нещо, което също е полезно за писане на проверяем код ).
Трудно е да си представим голям проект, основан на Принцип на единна отговорност без инжектиране на зависимост. Нека отново да разгледаме нашия клас OrderService:
public class OrderService { public void Create(...) { // Creating the order(and let’s forget about reserving here, it’s not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
Този код работи, но не е съвсем идеален. За да разберат как работи методът за създаване OrderService клас, те са принудени да разберат тънкостите на SMTP. И отново, copy-paste е единственият изход да се възпроизведе това използване на SMTP навсякъде, където е необходимо. Но с малко рефакторинг това може да се промени:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
Вече много по-добре! Но класът OrderService все още знае много за изпращането на имейли. Той се нуждае точно от клас SmtpMailer за изпращане на имейл. Ами ако искаме да го променим в бъдеще? Ами ако искаме да отпечатаме съдържанието на имейла, който се изпраща в специален регистрационен файл, вместо да ги изпратим в нашата среда за разработка? Какво ще стане, ако искаме да тестваме единично нашия клас OrderService? Нека продължим с рефакторинга, като създадем интерфейс IMailer:
public interface IMailer { void Send(string to, string subject, string body); }
SmtpMailer ще внедри този интерфейс. Също така, нашето приложение ще използва IoC-контейнер и ние можем да го конфигурираме така, че IMailer да бъде реализиран от класа SmtpMailer. След това OrderService може да бъде променен, както следва:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(, , ); } }
Сега стигаме някъде! Използвах този шанс, за да направя още една промяна. OrderService вече разчита на интерфейса IOrderRepository за взаимодействие с компонента, който съхранява всички наши поръчки. Вече не се интересува от това как е приложен този интерфейс и каква технология за съхранение го захранва. Сега класът OrderService има само код, който се занимава с бизнес логика на поръчките.
По този начин, ако тестер открие нещо, което се държи неправилно при изпращане на имейли, разработчикът знае къде точно да търси: клас SmtpMailer. Ако нещо не е наред с отстъпките, разработчикът отново знае къде да търси: OrderService (или в случай, че сте приели SRP наизуст, това може да е DiscountService) код на класа.
Все още обаче не харесвам метода OrderService.Create:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(, , ); }
Изпращането на имейл не е съвсем част от основния поток за създаване на поръчки. Дори ако приложението не успее да изпрати имейла, поръчката пак е създадена правилно. Също така, представете си ситуация, при която трябва да добавите нова опция в областта на потребителските настройки, която им позволява да се откажат от получаването на имейл след успешно направена поръчка. За да включим това в нашия клас OrderService, ще трябва да въведем зависимост, IUserParametersService. Добавете локализация към микса и имате още една зависимост, ITranslator (за да създадете правилни имейл съобщения на избрания от потребителя език). Няколко от тези действия са излишни, особено идеята да добавите тези много зависимости и да завършите с конструктор, който не се побира на екрана. Намерих a чудесен пример от това в кодовата база на Magento (популярна CMS за електронна търговия, написана на PHP) в клас, който има 32 зависимости!
в какво е програмиран windows
Понякога е трудно да се разбере как да се отдели тази логика и класът на Magento вероятно е жертва на един от тези случаи. Ето защо ми харесва начинът, управляван от събития:
namespace .Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Всеки път, когато се създава поръчка, вместо да се изпраща имейл директно от класа OrderService, се създава специален клас за събитие OrderCreated и се генерира събитие. Някъде в приложението ще бъдат конфигурирани манипулаторите на събития. Един от тях ще изпрати имейл до клиента.
namespace .EventHandlers { public class OrderCreatedEmailSender : IEventHandler { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
Класът OrderCreated е маркиран като сериализуем нарочно. Можем да се справим незабавно с това събитие или да го съхраним сериализирано в опашка (Redis, ActiveMQ или нещо друго) и да го обработим в процес / нишка, отделен от този, който обработва уеб заявките. В тази статия авторът обяснява подробно какво представлява управляваната от събития архитектура (моля, не обръщайте внимание на бизнес логиката в OrderController).
Някои може да твърдят, че сега е трудно да се разбере какво се случва, когато създавате поръчката. Но това не може да бъде по-далеч от истината. Ако се чувствате така, просто се възползвайте от функционалността на вашата IDE. Като намерим всички употреби на класа OrderCreated в IDE, можем да видим всички действия, свързани със събитието.
Но кога трябва да използвам инжектиране на зависимост и кога да използвам подход, управляван от събития? Не винаги е лесно да се отговори на този въпрос, но едно просто правило, което може да ви помогне, е да използвате инжекция на зависимостта за всичките си основни дейности в приложението и подход, управляван от събития, за всички вторични действия. Например използвайте Dependecy Injection с неща като създаване на поръчка в клас OrderService с IOrderRepository и делегирайте изпращането на имейл, нещо, което не е решаваща част от основния поток за създаване на поръчки, на някой манипулатор на събития.
Започнахме с много тежък контролер, само един клас, и завършихме със сложна колекция от класове. Предимствата на тези промени са съвсем очевидни от примерите. Все пак има много начини за подобряване на тези примери. Например методът OrderService.Create може да бъде преместен в собствен клас: OrderCreator. Тъй като създаването на поръчки е независима единица от бизнес логика, следваща Принципа на единна отговорност, съвсем естествено е тя да има свой собствен клас със собствен набор от зависимости. По същия начин премахването на поръчките и анулирането им могат да бъдат внедрени в своите класове.
Когато написах силно свързан код, нещо подобно на първия пример в тази статия, всяка малка промяна в изискването може лесно да доведе до много промени в други части на кода. SRP помага на разработчиците да пишат код, който е разделен, където всеки клас има своя собствена работа. Ако спецификациите на тази работа се променят, разработчикът прави промени само в този конкретен клас. Промяната е по-малко вероятно да наруши цялото приложение, тъй като другите класове все още трябва да си вършат работата както преди, освен ако, разбира се, не са били нарушени на първо място.
Разработването на код предварително с помощта на тези техники и спазването на Принципа на единна отговорност може да изглежда като обезсърчаваща задача, но усилията със сигурност ще се изплатят, тъй като проектът расте и развитието продължава.