توابع/متدها در Clean Code (قسمت 3)

خودداری از اثرات جانبی-Avoid side effectsخودداری از شرط های منفی-Avoid negative conditionalsخودداری از شرطی کردن-Avoid conditionalsخودداری از بررسی نوع-Avoid type-checking (دو قسمتی)خودداری از استفاده پرچم در پارامتر متد-Avoid flags in method parametersعدم نوشتن توابع گلوبال-Don't write to global functionsعدم استفاده از الگوی سینگلتون-Don't use a Singleton patternآرگومان های تابع(دو یا کمتر ایده آل است)-Function arguments (2 or fewer ideally)توابع باید یک کار را انجام دهند-Functions should do one thingاسامی تابع باید آنچه را انجام می دهند بگویند-Function names should say what they doتوابع فقط باید یک سطح انتزاع داشته باشند-Functions should only be one level of abstractionتابع صدا زننده و توابع فراخوانی شده باید نزدیک هم باشند-Function callers and called should be closeکپسوله کردن شرط ها-Encapsulate conditionalsحذف کدهای مرده-Remove dead codeخودداری از اثرات جانبی Avoid side effectsیک متد یک اثر جانبی ایجاد می کند اگر کاری غیر از گرفتن یک value ورودی و برگرداندن یک یا چند value دیگر انجام بدهد.یک اثر جانبی می تواند نوشتن یک فایل ، تغییر برخی متغیرهای global یا به طور تصادفی اتصال تمام پول شما به یک غریبه باشد.در حال حاضر ، لازم است اثر جانبی در یک برنامه داشته باشید.مانند مثال قبلی ، شاید لازم باشد که برای فایلی write کنید. در جایی که می خواهید این کار را انجام می دهید متمرکز شوید(centralize).چند متد و کلاس ندارید که به یک فایل خاص write می کند.یک سرویس داشته باشید که این کار را انجام دهد. فقط و فقط یکی.نکته اصلی پرهیز از بروز مشکلات عادی مانند تقسیم وضعیت بین اشیاء و بدون ساختار ، استفاده از انواع داده های قابل تغییر (mutable data types) که می توان برای هر چیزی نوشت ، جلوگیری کنید و عدم تمرکز در جایی که اثر جانبی شما رخ می دهد. اگر بتوانید این کار را انجام دهید ، از اکثر قریب به اتفاق سایر برنامه نویسان شادتر خواهید بود.Bad:// Global variable referenced by following function. // If we had another function that used this name, now it'd be an array and it could break it. var name = 'Ryan McDermott'; public string SplitIntoFirstAndLastName() { return name.Split(&quot &quot); } SplitIntoFirstAndLastName(); Console.PrintLine(name); // ['Ryan', 'McDermott'];Good:public string SplitIntoFirstAndLastName(string name) { return name.Split(&quot &quot); } var name = 'Ryan McDermott'; var newName = SplitIntoFirstAndLastName(name); Console.PrintLine(name); // 'Ryan McDermott'; Console.PrintLine(newName); // ['Ryan', 'McDermott'];خودداری از شرط های منفی Avoid negative conditionalsBad:public bool IsDOMNodeNotPresent(string node) { // ... } if (!IsDOMNodeNotPresent(node)) { // ... }Good:public bool IsDOMNodePresent(string node) { // ... } if (IsDOMNodePresent(node)) { // ... }خودداری از شرطی کردن Avoid conditionals این کار به نظر غیرممکن می رسد.پس از شنیدن این مورد ،اکثر مردم می گویند ،"چگونه قرار است بدون if کاری انجام دهم؟" پاسخ این است که در بسیاری از موارد می توانید برای دستیابی به همان وظیفه ، از polymorphism استفاده کنید.سوال دوم معمولاً ،"خوب این عالی است اما چرا می خواهم این کار را انجام دهم؟" جواب یک مفهوم کد تمیز قبلی است که ما آموخته ایم: یک متد فقط باید یک کار را انجام دهد.وقتی کلاس ها و متدهایی دارید که دارای عبارت if هستند ، به کاربر خود می گویید که متد شما بیش از یک کار را انجام می دهد.به یاد داشته باشید ، فقط یک کار را انجام دهید.Bad:class Airplane { // ... public double GetCruisingAltitude() { switch (_type) { case '777': return GetMaxAltitude() - GetPassengerCount(); case 'Air Force One': return GetMaxAltitude(); case 'Cessna': return GetMaxAltitude() - GetFuelExpenditure(); } } }Good:interface IAirplane { // ... double GetCruisingAltitude(); } class Boeing777 : IAirplane { // ... public double GetCruisingAltitude() { return GetMaxAltitude() - GetPassengerCount(); } } class AirForceOne : IAirplane { // ... public double GetCruisingAltitude() { return GetMaxAltitude(); } } class Cessna : IAirplane { // ... public double GetCruisingAltitude() { return GetMaxAltitude() - GetFuelExpenditure(); } }خودداری از بررسی نوع Avoid type-checking (قسمت اول)Bad:public Path TravelToTexas(object vehicle) { if (vehicle.GetType() == typeof(Bicycle)) { (vehicle as Bicycle).PeddleTo(new Location(&quottexas&quot)); } else if (vehicle.GetType() == typeof(Car)) { (vehicle as Car).DriveTo(new Location(&quottexas&quot)); } }Good:public Path TravelToTexas(Traveler vehicle) { vehicle.TravelTo(new Location(&quottexas&quot)); }or// pattern matching public Path TravelToTexas(object vehicle) { if (vehicle is Bicycle bicycle) { bicycle.PeddleTo(new Location(&quottexas&quot)); } else if (vehicle is Car car) { car.DriveTo(new Location(&quottexas&quot)); } }خودداری از بررسی نوع Avoid type-checking (قسمت دوم)Bad:public int Combine(dynamic val1, dynamic val2) { int value; if (!int.TryParse(val1, out value) || !int.TryParse(val2, out value)) { throw new Exception('Must be of type Number'); } return val1 + val2; }Good:public int Combine(int val1, int val2) { return val1 + val2; }خودداری از استفاده پرچم در پارامتر متد Avoid flags in method parametersیک flag نشان می دهد که این متد بیش از یک مسئولیت دارد.بهتر است که متد فقط یک مسئولیت واحد داشته باشد.یک پارامتر boolean مسئولیت های مختلفی را به متد اضافه می کند ، متد را به دو متد تقسیم کنید.Bad:public void CreateFile(string name, bool temp = false) { if (temp) { Touch(&quot./temp/&quot + name); } else { Touch(name); } }Good:public void CreateFile(string name) { Touch(name); } public void CreateTempFile(string name) { Touch(&quot./temp/&quot + name); }عدم نوشتن توابع گلوبال Don't write to global functionsهنوز تمام نشدهآلودگی globalها در بسیاری از زبانها روش بدی است زیرا می تواند با یک کتابخانه دیگر تداخل پیدا کند و کاربران API شما علاقه ای به رخ دادن خطا در محصول خود ندارند. بیایید در مورد یک مثال فکر کنیم: اگر می خواهید آرایه پیکربندی داشته باشید چه می کنید.شما می توانستید متد global مانند Config() بنویسید ، اما می تواند با یک کتابخانه دیگر که سعی در انجام همان کارها را داشت است، تداخل کند.Bad:public string[] Config() { return [ &quotfoo&quot => &quotbar&quot, ] }Good:class Configuration { private string[] _configuration = []; public Configuration(string[] configuration) { _configuration = configuration; } public string[] Get(string key) { return (_configuration[key]!= null) ? _configuration[key] : null; } }پیکربندی را Load کنید و نمونه کلاس Configuration را ایجاد کنیدvar configuration = new Configuration(new string[] { &quotfoo&quot => &quotbar&quot, });و اکنون شما باید از نمونه Configuration در برنامه خود استفاده کنید.عدم استفاده از الگوی سینگلتون Don't use a Singleton patternالگوی Singleton یک ضد الگوی است(anti-pattern).تفسیر از Brian Button:1-آنها به عنوان یک global instance مورد استفاده قرار می گیرند ،چرا اینقدر بد است؟از آنجا که وابستگی برنامه را در کد پنهان می کنید ،به جای نمایش آنها از طریق interface ها. ایجاد چیزی global برای جلوگیری از عبور آن در اطراف بوی کد/code smell است.2-آنها اصل مسئولیت واحد/single responsibility principle را نقض می کنند:به این دلیل که آنها کنترل ایجاد و چرخه زندگی خود را کنترل می کنند.3-اینها ذاتاً باعث می شوند كه كد به هم اتصال محکم و سفت /tightly coupled داشته باشند.این امر باعث می شود که در بسیاری از موارد test آنها بسیار دشوار شود.4-آنها حول و حوش lifetime برنامه کاربرد دارند.ضربه دیگر به test کردن از آنجا که می توانید با شرایطی روبرو شوید که تست ها باید سفارشی شوند که به بزرگی unit test ها نیستند. چرا؟ زیرا هر تست واحد باید از دیگری مستقل باشد.همچنین خاطرات بسیار خوبی توسط Misko Hevery در مورد ریشه مشکل/root of problem وجود دارد.Bad:class DBConnection { private static DBConnection _instance; private DBConnection() { // ... } public static GetInstance() { if (_instance == null) { _instance = new DBConnection(); } return _instance; } // ... } var singleton = DBConnection.GetInstance();Good:class DBConnection { public DBConnection(IOptions<DbConnectionOption> options) { // ... } // ... }نمونه ای از کلاس DBConnection ایجاد کنید و آن را با Option pattern پیکربندی کنید.var options = <resolve from IOC>; var connection = new DBConnection(options);و اکنون شما باید از DBConnection در برنامه خود استفاده کنید.آرگومان های تابع(دو یا کمتر ایده آل است)محدود کردن مقدار پارامترهای تابع یا متد فوق العاده مهم است زیرا test متد شما را آسان تر می کند.داشتن بیش از سه مورد منجر به یک انفجار ترکیبی می شود که در آن باید موارد مختلفی را با هر آرگومان جداگانه test کنید.آرگومان صفر مورد ایده آل است.یک یا دو آرگومان خوب است ،و از سه آرگومان باید اجتناب شود.هر چیزی بیشتر از این تعداد باشد باید یکی شوند.معمولاً اگر بیش از دو آرگومان داشته باشید ، متد شما در تلاش بیش از حد است.در مواردی که اینگونه نباشد ، بیشتر اوقات یک شی سطح بالاتر به عنوان یک آرگومان کافی خواهد بود.Bad:public void CreateMenu(string title, string body, string buttonText, bool cancellable) { // ... }Good:pubic class MenuConfig { public string Title { get; set; } public string Body { get; set; } public string ButtonText { get; set; } public bool Cancellable { get; set; } } var config = new MenuConfig { Title = &quotFoo&quot, Body = &quotBar&quot, ButtonText = &quotBaz&quot, Cancellable = true }; public void CreateMenu(MenuConfig config) { // ... }توابع باید یک کار را انجام دهند Functions should do one thingاین مهمترین قانون در مهندسی نرم افزار است.وقتی متدها بیش از یک کار را انجام دهند ، نوشتن ، آزمایش و استدلال در مورد آنها سخت تر است.هنگامی که می توانید یک عملکرد را تنها با یک عمل جدا کنید ، می توان آنها را به راحتی refactored کرد و کد شما بسیار تمیز تر خوانده می شود.اگر این راهنما را رعایت کنید، شما از بسیاری از توسعه دهندگان جلوتر خواهید بود.Bad:public void SendEmailToListOfClients(string[] clients) { foreach (var client in clients) { var clientRecord = db.Find(client); if (clientRecord.IsActive()) { Email(client); } } }Good:public void SendEmailToListOfClients(string[] clients) { var activeClients = GetActiveClients(clients); // Do some logic } public List<Client> GetActiveClients(string[] clients) { return db.Find(clients).Where(s => s.Status == &quotActive&quot); }اسامی تابع باید آنچه را انجام می دهند بگویند Function names should say what they doBad:public class Email { //... public void Handle() { SendMail(this._to, this._subject, this._body); } } var message = new Email(...); // What is this? A handle for the message? Are we writing to a file now? message.Handle();Good:public class Email { //... public void Send() { SendMail(this._to, this._subject, this._body); } } var message = new Email(...); // Clear and obvious message.Send();توابع فقط باید یک سطح انتزاع داشته باشند Functions should only be one level of abstractionهنوز تمام نشدههنگامی که بیش از یک سطح انتزاع دارید متد شما معمولاً خیلی زیاد انجام می شود.تقسیم توابع منجر به قابلیت استفاده مجدد و آزمایش آسانتر می شود.Bad:public string ParseBetterJSAlternative(string code) { var regexes = [ // ... ]; var statements = explode(&quot &quot, code); var tokens = new string[] {}; foreach (var regex in regexes) { foreach (var statement in statements) { // ... } } var ast = new string[] {}; foreach (var token in tokens) { // lex... } foreach (var node in ast) { // parse... } }ما برخی از functionality را انجام داده ایم ، اما عملکرد ParseBetterJSAlternative() هنوز بسیار پیچیده است و قابل آزمایش نیست.Bad too:public string Tokenize(string code) { var regexes = new string[] { // ... }; var statements = explode(&quot &quot, code); var tokens = new string[] {}; foreach (var regex in regexes) { foreach (var statement in statements) { tokens[] = /* ... */; } } return tokens; } public string Lexer(string[] tokens) { var ast = new string[] {}; foreach (var token in tokens) { ast[] = /* ... */; } return ast; } public string ParseBetterJSAlternative(string code) { var tokens = Tokenize(code); var ast = Lexer(tokens); foreach (var node in ast) { // parse... } }بهترین راه حل این است که وابستگی های تابع ParseBetterJSAlternative() را خارج کنید.Good:class Tokenizer { public string Tokenize(string code) { var regexes = new string[] { // ... }; var statements = explode(&quot &quot, code); var tokens = new string[] {}; foreach (var regex in regexes) { foreach (var statement in statements) { tokens[] = /* ... */; } } return tokens; } } class Lexer { public string Lexify(string[] tokens) { var ast = new[] {}; foreach (var token in tokens) { ast[] = /* ... */; } return ast; } } class BetterJSAlternative { private string _tokenizer; private string _lexer; public BetterJSAlternative(Tokenizer tokenizer, Lexer lexer) { _tokenizer = tokenizer; _lexer = lexer; } public string Parse(string code) { var tokens = _tokenizer.Tokenize(code); var ast = _lexer.Lexify(tokens); foreach (var node in ast) { // parse... } } }تابع صدا زننده و توابع فراخوانی شده باید نزدیک هم باشند Function callers and called should be closeاگر یک تابع، تابع دیگری را صدا می کند ، آن توابع را به صورت عمودی در فایل منبع نگه دارید.در حالت ایده آل ، متد صدا زننده را درست بالای متد فراخوانی شده نگه دارید.ما تمایل داریم مانند روزنامه کد را از بالا به پایین بخوانیم.به همین دلیل ، کد خود را به این روش بخوانید.Bad:class PerformanceReview { private readonly Employee _employee; public PerformanceReview(Employee employee) { _employee = employee; } private IEnumerable<PeersData> LookupPeers() { return db.lookup(_employee, 'peers'); } private ManagerData LookupManager() { return db.lookup(_employee, 'manager'); } private IEnumerable<PeerReviews> GetPeerReviews() { var peers = LookupPeers(); // ... } public PerfReviewData PerfReview() { GetPeerReviews(); GetManagerReview(); GetSelfReview(); } public ManagerData GetManagerReview() { var manager = LookupManager(); } public EmployeeData GetSelfReview() { // ... } } var review = new PerformanceReview(employee); review.PerfReview();Good:class PerformanceReview { private readonly Employee _employee; public PerformanceReview(Employee employee) { _employee = employee; } public PerfReviewData PerfReview() { GetPeerReviews(); GetManagerReview(); GetSelfReview(); } private IEnumerable<PeerReviews> GetPeerReviews() { var peers = LookupPeers(); // ... } private IEnumerable<PeersData> LookupPeers() { return db.lookup(_employee, 'peers'); } private ManagerData GetManagerReview() { var manager = LookupManager(); return manager; } private ManagerData LookupManager() { return db.lookup(_employee, 'manager'); } private EmployeeData GetSelfReview() { // ... } } var review = new PerformanceReview(employee); review.PerfReview();کپسوله کردن شرط ها Encapsulate conditionalsBad:if (article.state == &quotpublished&quot) { // ... }Good:if (article.IsPublished()) { // ... }حذف کدهای مرده Remove dead codeکد مرده به همان اندازه کد تکراری بد است.هیچ دلیلی برای نگه داشتن آن در codebase شما وجود ندارد.اگر آن را صدا نمیزنید ، از شر آن خلاص شوید! اگر هنوز به آن نیاز دارید ، در version history شما ایمن خواهد بود.Bad:public void OldRequestModule(string url) { // ... } public void NewRequestModule(string url) { // ... } var request = NewRequestModule(requestUrl); InventoryTracker(&quotapples&quot, request, &quotwww.inventory-awesome.io&quot);Good:public void RequestModule(string url) { // ... } var request = RequestModule(requestUrl); InventoryTracker(&quotapples&quot, request, &quotwww.inventory-awesome.io&quot);منبع این بخش