設計原則之單一職責原則
使用這些技術并遵循“單一職責原則”預先開發(fā)代碼似乎是一項艱巨的任務,但是隨著項目的發(fā)展和開發(fā)的繼續(xù),這些努力肯定會得到回報。
無論我們認為什么是出色的代碼,它始終需要一種簡單的質(zhì)量:代碼必須是可維護的。正確的縮進,整潔的變量名,100%的測試覆蓋率等等只能使您走得更遠。任何無法維護且不能相對輕松地適應不斷變化的需求的代碼都是等待過時的代碼。當我們嘗試構(gòu)建原型,概念驗證或最低限度的產(chǎn)品時,我們可能不需要編寫出色的代碼,但是在所有其他情況下,我們應始終編寫可維護的代碼。這應該被視為軟件工程和設計的基本素質(zhì)。
單一責任原則:偉大守則的秘訣
在本文中,我將討論“單一職責原則”以及圍繞它的一些技術如何使您的代碼具有如此高的質(zhì)量。編寫出色的代碼是一門藝術,但是某些原則始終可以幫助您為開發(fā)工作提供開發(fā)健壯且可維護的軟件所需的方向。
模型就是一切
幾乎每本有關某些新MVC(MVP,MVVM或其他M **)框架的書都充斥著不良代碼示例。這些示例試圖說明框架必須提供的功能。但是他們最終也為初學者提供了不好的建議?!白屛覀兊哪P蛽碛羞@個ORM X,為我們的視圖模板化引擎Y,并且我們將有控制器來管理所有這些”之類的例子,除了巨大的控制器之外,什么都沒有實現(xiàn)。
盡管為這些書辯護,但這些示例旨在說明您可以輕松地開始使用它們的框架。它們無意教軟件設計。但是,跟隨這些示例的讀者僅在數(shù)年后才意識到,在他們的項目中包含大量的代碼塊會適得其反。
模型是您應用程序的核心。
模型是您應用程序的核心。如果您將模型與應用程序邏輯的其余部分分開,則無論您的應用程序變得多么復雜,維護都將更加容易。即使對于復雜的應用程序,良好的模型實現(xiàn)也可能導致代碼表現(xiàn)力強。為了實現(xiàn)這一目標,首先要確保您的模型只執(zhí)行其應做的事情,而不用擔心圍繞它構(gòu)建的應用程序會做什么。此外,它并不關心底層數(shù)據(jù)存儲層是什么:您的應用程序依賴于SQL數(shù)據(jù)庫還是將所有內(nèi)容存儲在文本文件中?
當我們繼續(xù)本文時,您將意識到關于關注點分離的代碼是多么偉大。
單一責任原則
您可能已經(jīng)聽說過SOLID原則:單一職責,開放式,封閉式,liskov替換,接口隔離和依賴倒置。第一個字母S代表單一責任原則(SRP),其重要性不可高估。我什至會爭辯說,這是良好代碼的必要和充分條件。實際上,在任何編寫不好的代碼中,您總能找到一個承擔多個職責的類-包含數(shù)千行代碼的form1.cs或index.php并非難事,我們所有人可能已經(jīng)看過或做了。
讓我們看一下C#中的示例(ASP.NET MVC和實體框架)。即使您不是C#開發(fā)人員,如果您具有一些OOP經(jīng)驗,也可以輕松地進行后續(xù)操作。
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類,顯示了它的Create方法。在這樣的控制器中,我經(jīng)??吹綄rder類本身用作請求參數(shù)的情況。但是我更喜歡使用特殊的請求類。同樣,SRP!
一個控制器的作業(yè)太多
注意上面的代碼片段中,控制器如何對“下訂單”了解太多,包括但不限于存儲Order對象,發(fā)送電子郵件等。對于單個類來說,這簡直就是太多工作。對于每一個小的更改,開發(fā)人員都需要更改整個控制器的代碼。萬一另一個Controller也需要創(chuàng)建訂單,開發(fā)人員通常會復制粘貼代碼,以防萬一??刂破鲬獌H控制整個過程,而不能真正容納過程的每一個邏輯。
但是今天是我們停止編寫這些龐大控制器的一天!
讓我們首先從控制器中提取所有業(yè)務邏輯,然后將其移至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");
}
完成此操作后,控制器現(xiàn)在僅執(zhí)行打算執(zhí)行的操作:控制過程。它僅了解視圖,OrderService和OrderRequest類-它完成其工作所需的最少信息集,即管理請求和發(fā)送響應。
這樣,您將很少更改控制器代碼。其他組件(例如視圖,請求對象和服務)仍可以更改,因為它們鏈接到業(yè)務需求,而不是控制器。
這就是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();
}
}
該代碼有效,但不是很理想。為了了解創(chuàng)建方法OrderService類的工作方式,他們被迫了解SMTP的復雜性。而且,復制粘貼是在任何需要的地方復制此SMTP使用的唯一出路。但是經(jīng)過一些重構(gòu),情況可能會改變:
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類仍然對發(fā)送電子郵件了解很多。它完全需要SmtpMailer類來發(fā)送電子郵件。如果我們將來要更改該怎么辦?如果我們要打印發(fā)送到特殊日志文件中的電子郵件的內(nèi)容,而不是在我們的開發(fā)環(huán)境中實際發(fā)送它們,該怎么辦?如果我們要對OrderService類進行單元測試該怎么辦?讓我們繼續(xù)通過創(chuàng)建接口IMailer進行重構(gòu):
public interface IMailer
{
void Send(string to, string subject, string body);
}
SmtpMailer將實現(xiàn)此接口。另外,我們的應用程序?qū)⑹褂肐oC容器,并且可以對其進行配置,以使IMailer由SmtpMailer類實現(xiàn)。然后可以如下更改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(<orders user email>, <subject>, <body with order details>);
}
}
現(xiàn)在我們到了某個地方!我借此機會也做了另一番改變?,F(xiàn)在,OrderService依靠IOrderRepository接口與存儲所有訂單的組件進行交互。它不再關心該接口的實現(xiàn)方式以及為其提供支持的存儲技術?,F(xiàn)在,OrderService類僅具有處理訂單業(yè)務邏輯的代碼。
這樣,如果測試人員發(fā)現(xiàn)發(fā)送電子郵件時行為不正確的內(nèi)容,則開發(fā)人員會確切知道該看哪里:SmtpMailer類。如果折扣出了點問題,開發(fā)人員又會知道在哪里尋找:OrderService(或者,如果您內(nèi)心深信SRP,則可能是DiscountService)類代碼。
事件驅(qū)動架構(gòu)
但是,我仍然不喜歡OrderService.Create方法:
public void Create(...)
{
var order = new Order();
...
this.repository.Save(order);
this.mailer.Send(<orders user email>, <subject>, <body with order details>);
}
發(fā)送電子郵件并不是主要訂單創(chuàng)建流程的一部分。即使該應用程序無法發(fā)送電子郵件,訂單仍然可以正確創(chuàng)建。另外,設想一種情況,您必須在用戶設置區(qū)域中添加一個新選項,使他們在成功下訂單后可以選擇退出接收電子郵件。要將其合并到我們的OrderService類中,我們將需要引入一個依賴項IUserParametersService。將本地化添加到混合中,您還有另一個依賴項ITranslator(以用戶選擇的語言生成正確的電子郵件)。這些動作中的幾個動作是不必要的,特別是添加許多依賴關系并最終得到一個不適合屏幕的構(gòu)造函數(shù)的想法。我找到了一個很好的例子 在Magento的代碼庫中(一個用PHP編寫的流行電子商務CMS),該類具有32個依賴項!
屏幕上不適合的構(gòu)造函數(shù)
有時很難弄清楚如何分離這種邏輯,Magento的班級可能是其中一種情況的受害者。這就是為什么我喜歡事件驅(qū)動的方式:
namespace <base namespace>.Events
{
[Serializable]
public class OrderCreated
{
private readonly Order order;
public OrderCreated(Order order)
{
this.order = order;
}
public Order GetOrder()
{
return this.order;
}
}
}
每當創(chuàng)建訂單時,都將創(chuàng)建特殊事件類OrderCreated并生成事件,而不是直接從OrderService類發(fā)送電子郵件。在應用程序中的某個地方將配置事件處理程序。其中之一將向客戶發(fā)送電子郵件。
namespace <base namespace>.EventHandlers
{
public class OrderCreatedEmailSender : IEventHandler<OrderCreated>
{
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類被故意標記為可序列化。我們可以立即處理此事件,或?qū)⑵湫蛄谢鎯υ陉犃兄校≧edis,ActiveMQ或其他),并在與處理Web請求的進程/線程分開的進程/線程中進行處理。在本文中,作者詳細解釋了什么是事件驅(qū)動的體系結(jié)構(gòu)(請不要關注OrderController中的業(yè)務邏輯)。
有人可能會爭辯說,現(xiàn)在很難理解創(chuàng)建訂單時的情況。但這與事實不符。如果您有這種感覺,只需利用IDE的功能即可。通過在IDE中找到OrderCreated類的所有用法,我們可以看到與該事件關聯(lián)的所有動作。
但是,什么時候應該使用依賴注入,什么時候應該使用事件驅(qū)動的方法?回答這個問題并不總是那么容易,但是可以幫助您的一個簡單規(guī)則是對應用程序中的所有主要活動使用依賴注入,對所有輔助操作使用事件驅(qū)動的方法。例如,將Dependecy Injection與諸如使用IOrderRepository在OrderService類內(nèi)創(chuàng)建訂單,以及將不是主要訂單創(chuàng)建流程的關鍵部分的電子郵件委托給某個事件處理程序的操作結(jié)合使用。
結(jié)論
我們從一個非常繁重的控制器開始,只有一個類,最后是精心制作的類集合。這些變化的優(yōu)勢從示例中顯而易見。但是,仍有許多方法可以改進這些示例。例如,可以將OrderService.Create方法移至其自己的類:OrderCreator。由于訂單創(chuàng)建是遵循“單一職責原則”的業(yè)務邏輯的獨立單元,因此自然而然會有一個具有自己的依賴關系集的類。同樣,訂單刪除和訂單取消可以分別在自己的類中實現(xiàn)。
當我編寫高度耦合的代碼時(類似于本文的第一個示例),對需求的任何細微更改都可能輕易導致代碼其他部分的許多更改。SRP幫助開發(fā)人員編寫解耦的代碼,其中每個類都有其自己的工作。如果此作業(yè)的規(guī)范發(fā)生更改,則開發(fā)人員僅更改該特定類。這種更改不太可能破壞整個應用程序,因為其他類當然應該像以前一樣繼續(xù)工作,除非它們當然首先被破壞了。
使用這些技術并遵循“單一職責原則”預先開發(fā)代碼似乎是一項艱巨的任務,但是隨著項目的發(fā)展和開發(fā)的繼續(xù),這些努力肯定會得到回報。
