Глава 21. Качествен програмен код

В тази тема...

В настоящата тема ще разгледаме основните правила за писане на качествен програмен код. Ще бъде обърнато внимание на именуването на елементите от програмата (променливи, методи, класове и други), прави­лата за форматиране и подреждане на кода, добрите практики за изграж­дане на висококачествени методи и принципите за качествена докумен­тация на кода. Ще бъдат дадени много примери за качествен и некаче­ствен код. Ще бъдат описани и официалните "Design Guidelines for Developing Class Libraries за .NET" от Майкрософт. В процеса на работа ще бъде обяснено как да се използва средата за програмиране, за да се автомати­зират някои операции като форма­тиране и преработка на кода.

Тази тема се базира на предходната – "Принципи на Обектно-ориенти­раното програмиране" и очаква читателят да е запознат с основните ООП принципи: Абстракция, наследяване, полиморфизъм и капсулация, които имат огромно значение върху качеството на кода.

Съдържание

Видео

Презентация

Мисловни карти


Защо качеството на кода е важно?

Нека разгледаме следния код:

static void Main()

{

  int value=010, i=5, w;

  switch(value){case 10:w=5;Console.WriteLine(w);break;case 9:i=0;break;

          case 8:Console.WriteLine("8 ");break;

     default:Console.WriteLine("def ");{

              Console.WriteLine("hoho "); }

     for (int k = 0; k < i; k++, Console.WriteLine(k - 'f'));break;} { Console.WriteLine("loop!"); }

}

Можете ли от първия път да познаете какво прави този код? Дали го прави правилно или има грешки?

Какво е качествен програмен код?

Качеството на една програма има два аспекта – качеството, измерено през призмата на потребителя (наречено външно качество), и от гледна точка на вътрешната организация (наречено вътрешно качество).

Външното качество зависи от това колко коректно работи тази програма. Зависи също от това колко е интуитивен и ползваем е потребителският интерфейс. Зависи и от производителността (колко бързо се справя тя с поставените задачи).

Вътрешното качество е свързано с това колко добре е построена тази програма. То зависи от архитектурата и дизайна (дали са достатъчно изчистени и подходящи). Зависи от това колко лесно е да се направи промяна или добавяне на нова функционалност (леснота за поддръжка). Зависи и от простотата на реализацията и четимостта на кода. Вътреш­ното качество е свързано най-вече с кода на програмата.

Характеристики за качество на кода

Качествен програмен код е такъв, който се чете и разбира лесно. Качествен код е такъв, който се модифицира и поддържа лесно и праволинейно. Той трябва да е коректен при всякакви входни данни, да е добре тестван. Трябва да има добра архитектура и дизайн. Документацията трябва да е на ниво или поне кодът да е самодокументиращ се. Трябва да има добро форматиране, което консистентно се прилага навсякъде.

На всички нива (модули, класове, методи) трябва да има висока свързаност на отговорностите (strong cohesion) – едно парче код трябва да върши точно едно определено нещо.

Функцио­налната независимост (loose coupling) между модули, класове и методи е от изключителна важност. Подходящо и консистентно имену­ване на класовете, методите, променливите и останалите елементи също е задължително условие. Кодът трябва да има и добра документация, вгра­дена в него самия.

Защо трябва да пишем качествено?

Нека погледнем този код отново:

static void Main()

{

  int value=010, i=5, w;

  switch(value){case 10:w=5;Console.WriteLine(w);break;case 9:i=0;break;

          case 8:Console.WriteLine("8 ");break;

     default:Console.WriteLine("def ");{

              Console.WriteLine("hoho "); }

     for (int k = 0; k < i; k++, Console.WriteLine(k - 'f'));break;} { Console.WriteLine("loop!"); }

}

Можете ли да кажете дали този код се компилира без грешки? Можете ли да кажете какво прави само като го гледате? Можете ли да добавите нова функционалност и да сте сигурни, че няма да счупите нищо старо? Можете ли да кажете за какво служи променливата k или променливата w?

Във Visual Studio има опция за пренареждане на код. Ако горният код бъде сложен в Visual Studio и се извика тази опция (клавишна комбинация [Ctrl+K, Ctrl+F]), кодът ще бъде преформатиран и ще изглежда съвсем различно. Въпреки това все още няма да е ясно за какво служат промен­ливите, но поне ще е ясно кой блок с код къде завършва:

static void Main()

{

      int value = 010, i = 5, w;

      switch (value)

      {

            case 10: w = 5; Console.WriteLine(w); break;

            case 9: i = 0; break;

            case 8: Console.WriteLine("8 "); break;

            default: Console.WriteLine("def ");

                  {

                        Console.WriteLine("hoho ");

                  }

                  for (int k = 0; k < i; k++, Console.WriteLine(k - 'f')) ; break;

      } { Console.WriteLine("loop!"); }

}

Ако всички пишеха код както в примера, нямаше да е възможно реализирането на големи и сериозни софтуерни проекти, защото те се пишат от големи екипи от софтуерни инженери. Ако кодът на всички е като в примера по-горе, никой няма да е в състояние да разбере как работи (и дали работи) кодът на другите от екипа, а с голяма вероятност никой няма да си разбира и собствения код.

С времето в професията на програмистите се е натрупал сериозен опит и добри практики за писане на качествен програмен код, за да е възможно всеки да разбере кода на колегите си и да може да го променя и дописва. Тези практики представляват множество от препоръки и правила за форматиране на кода, за именуване на идентификаторите и за правилно структуриране на програмата, които правят писането на софтуер по-лесно. Качественият и консистентен код помага най-вече за поддръжката и лесната промяна. Качественият код е гъвкав и стабилен. Той се чете и разбира лесно от всички. Ясно е какво прави от пръв поглед, поради това е самодокументиращ се. Качественият код е интуитивен – ако не го позна­вате има голяма вероятност да познаете какво прави само с един бърз поглед. Качественият код е удобен за преизползване, защото прави само едно нещо (strong cohesion), но го прави добре, като разчита на минима­лен брой други компоненти (loose coupling) и ги използва само през публичните им интерфейси. Качественият код спестява време и труд и прави написания софтуер по-ценен.

Някои програмисти гледат на качествения код като на прекалено прост. Не смятат, че могат да покажат знанията си с него. И затова пишат трудно четим код, който използва характеристики, които не са добре документирани или не са популярни. Пишат функции на един ред. Това е изключително грешна практика.

Код-конвенции

Преди да продължим с препоръките за писане на качествен програмен код ще поговорим малко за код-конвенции. Код-конвенция е група правила за писане на код, използвана в рамките на даден проект или организация. Те могат да включват правила за именуване, форматиране и логическа подредба. Едно такова правило например може да препоръчва класовете да започват с главна буква, а променливите – с малка. Друго правило може да твърди, че къдравата скоба за нов блок с програмни конструкции се слага на същия ред, а не на нов ред.

clip_image001

Неконсистентното използване на една конвенция е по-лошо и по-опасно от липсата на конвенция въобще.

Конвенциите са започнали да се появяват в големи и сериозни проекти, в които голям брой програмисти са пишели със собствен стил и всеки от тях е спазвал собствени (ако въобще е спазвал някакви) правила. Това е правело кода по-трудно четим и е принудило ръководителите на проек­тите да въведат писани правила. По-късно най-добрите код конвенции са придобили популярност и са станали де факто стандарт.

Microsoft има официална код конвенция наречена Design Guidelines for Developing Class Libraries (http://msdn.microsoft.com/en-us/library/ms229042(VS.100).aspx за .NET Framework 4.0).

От тогава тази код конвенция е добила голяма популярност и е широко разпространена. Правилата за именуване на идентификато­рите и за форматиране на кода, които ще дадем в тази тема, са в синхрон с код конвенцията на Microsoft.

Големите организации спазват стриктни конвенции, като конвенциите в отделните екипи могат да варират. Повечето водачи на екипи избират да спазват официалната конвенция на Microsoft като в случаите в които тя не е достатъчна се разширява според нуждите.

clip_image001[1]

Качеството на кода не е група конвенции, които трябва да се спазват, то е начин на мислене.

Управление на сложността

Управлението на сложността играе централна роля в писането на софтуер. Основната цел е да се намали сложността, с която всеки трябва да се справя. Така мозъкът на всеки един от участниците в създаването на софтуер се налага да мисли за много по-малко неща.

Управлението на сложността започва от архитектурата и дизайна. Всеки един от модулите (или автономните единици код) или дори класовете трябва да са проектирани, така че да намаляват сложността.

Добрите практики трябва да се прилагат на всяко ниво – класове, методи, член-променливи, именуване, оператори, управление на грешките, форматиране, коментари. Добрите практики са в основата на намаляване на сложността. Те канализират много решения за кода по строго определени правила и така помагат на всеки един разработчик да мисли за едно нещо по-малко докато чете и пише код.

За управлението на сложността може да се гледа и от частното към общото: за един разработчик е изключително полезно да може да се абстрахира от голямата картина, докато пише едно малко парче код. За да е възможно това, парчето код трябва да е с достатъчно ясни очертания съобразени с голямата картина. Важи римското правило - разделяй и владей, но отнесено към сложността.

Правилата, за които ще говорим по-късно са насочени точно към това, да се намери начин цялостната сложност да бъде "изключена" докато се работи над една малка част от системата.

Именуване на идентификаторите

Идентификатори са имената на класове, интерфейси, изброими типове, анотации, методи и променливи. В C# и в много други езици имената на идентификаторите се избират от разработчика. Имената не трябва да бъдат случайни. Те трябва да са съставени така, че да носят полезна информация за какво служат и каква точно роля изпълняват в съответния код. Така кодът става по-лесно четим.

Когато именуваме идентификатори е добре да си задаваме въпроси: Какво прави този клас? Каква е целта на тази променлива? За какво се използва този метод?

Добри имена са:

FactorialCalculator, studentsCount, Math.PI, configFileName, CreateReport

Лоши имена са:

k, k2, k3, junk, f33, KJJ, button1, variable, temp, tmp, temp_var, something, someValue

Изключително лошо име на клас или метод е Problem12. Някои начинаещи програ­мисти дават такова име за решението на задача 12 от упражненията. Това е изключително грешно! Какво ще ви говори името Problem12 след 1 седмица или след 1 месец? Ако задачата търси път в лабиринт, дайте и име PathInLabyrinth. След 3 месеца може да имате подобна задача и да трябва да намерите задачата за лабиринта. Как ще я намерите, ако не сте й дали подходящо име? Не давайте име, което съдържа числа – това е индикация за лошо именуване.

clip_image001[2]

Името на идентификаторите трябва да описва за какво служи този клас. Решението на задача 12 от упражне­нията не трябва да се казва Problem12 или Zad12. Това е груба грешка!

Избягвайте съкращения

Съкращения трябва се избягват, защото могат да бъдат объркващи. Например за какво ви говори името на клас GrBxPnl? Не е ли по-ясно, ако името е GroupBoxPanel? Изключения се правят за акроними, които са по-попу­лярни от пълната си форма, например HTML или URL. Например името HTMLParser е препоръчително пред HyperTextMarkupLanguageParser.

Английски език

Едно от най-основните правила е, винаги да се използва английски език. Помислете само ако някой виетнамец използва виетнамски език, за да си кръщава променливите и методите. Какво ще разберете, ако четете неговия код? Ами какво ще разбере виетнамецът, ако вие сте ползвали български и след това се наложи той да допише вашия код. Единственият език, който всички програмисти владеят, е английският.

clip_image001[3]

Английският език е де факто стандарт при писането на софтуер. Винаги използвайте английски език за имената на идентификаторите в сорс кода (променливи, методи, класове и т.н.). Използвайте английски и за коментарите в програмата.

Нека сега разгледаме как да подберем подходящите идентификатори в различните случаи.

Последователност при именуването

Начинът на именуване трябва да е последователен.

В групата методи LoadFile(), LoadImageFromFile(), LoadSettings(), LoadFont(), LoadLibrary() е неправилно да се включи и ReadTextFile().

Противоположните дейности трябва да симетрично именувани (тоест, когато знаете как е именувана една дейност, да можете да предположите как е именувана противоположната дейност): LoadLibrary() и UnloadLibrary(), но не и FreeHandle(). Също и OpenFile() с CloseFile(), но не и DeallocateResource(). Към двойката GetName,  SetName е неестествено да се добави AssignName.

Забележете, че в CTS големи групи класове имат последователно именуване: колекциите (пакетът и всички класове използват думите Collection и List и никога не използват техни синоними), потоците винаги са Streams.

clip_image001[4]

Именувайте последователно – не използвайте синоними. Именувайте противоположностите симетрично.

Имена на класове, интерфейси и други типове

От главата "Принципи на обектно-ориентираното програмиране" знаем, че класовете описват обекти от реалния свят. Имената на класовете трябва да са съставени от съществително име (нарицателно или собствено), като може да има едно или няколко прилагателни (преди или след същест­вителното). Например класът описващ Африканския лъв ще се казва AfricanLion. Тази нотация на именуване се нарича Pascal Case – първата буква на всяка дума от името е главна, а останалите са малки. Така по-лесно се чете (за да се убедите в това, забележете разликата между името idatagridcolumnstyleeditingnotificationservice срещу името IDataGridColumnStyleEditingNotificationService). Последното име е на публичния клас с най-дълго име в .NET Framework (46 знака, от System.Windows.Forms).

Да дадем още няколко примера. Трябва да напишем клас, който намира прости числа в даден интервал. Добро име за този клас е PrimeNumbers или PrimeNumbersFinder или PrimeNumbersScanner. Лоши имена биха могли да бъдат FindPrimeNumber (не трябва да ползваме глагол за име на клас) или Numbers (не става ясно какви числа и какво ги правим) или Prime (не трябва името на клас да е прилагателно).

Колко да са дълги имената на класовете?

Имената на класовете не трябва да надвишават в общия случай 20 символа, но понякога това правило не се спазва, защото се налага да се опише обект от реалността, който се състои от няколко дълги думи. Както видяхме по-горе има класове и с по 46 знака. Въпреки дължината е ясно за какво този клас. По тази причината препоръката за дължина до 20 символа, е само ориентировъчна, а не задължителна. Ако може едно име да е по-кратко и също толкова ясно, колкото дадено по-дълго име, предпочитайте по-краткото.

Лош съвет би бил да се съкращава, за да се поддържат имената кратки. Следните имена достатъчно ясни ли са: CustSuppNotifSrvc, FNFException? Очевидно не са. Доста по-ясни са FileNotFoundException, CustomerSupportNotificationService, въпреки че са по-дълги.

Имена на интерфейси и други типове

Имената на интерфейсите трябва да следват същата конвенция, както имената на класовете: изписват се в Pascal Case и се състоят от съществително и евентуално прилагателни. За да се различават от останалите типове, конвенцията повелява да се сложи префикс I.

Примери са IEnumerable, IFormattable, IDataReader, IList, IHttpModule, ICommandExecutor.

Лоши примери са: List, FindUsers, IFast, IMemoryOptimize, Optimizer, FastFindInDatabase, CheckBox.

В .NET има още една нотация за имена интерфейси: да завършват на "able": Runnable, Serializable, Cloneable. Това са интер­фейси, които най-често добавят допълнителна роля към основната роля на един обект. Повечето интерфейси обаче не следват тази нотация, например интерфейсите IList и ICollection.

Имена на изброимите типове (enumerations)

Няколко формата са допустими: [Съществително] или [Глагол] или [Прилагателно]. Имената им са в единствено или множествено число. За всички членове на изброимите типове трябва да се спазва един и същ стил.

enum Days

{

      Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday

};

Имена на атрибути

Имената на атрибутите трябва да имат окончание Attribute. Например WebServiceAttribute.

Имена на изключения

Код конвенцията повелява изключенията да завършват на Exception. Името трябва да е достатъчно информативно. Добър пример би бил FileNotFoundException. Лош би бил FileNotFoundError.

Имена на делегати

Делегатите трябва да имат суфикс Delegate или EventHandler. DownloadFinishedDelegate би бил добър пример, докато WakeUpNotification не би спазвал конвенцията.

Имена на пакети

Пакетите (namespaces, обяснени в главата "Създаване и използване на обекти") използват PascalCase за именуване, също като класовете. Следните формати са за предпочитане: Company.Product.Component... и Product.Component... .

Добър пример: Telerik.WinControls.GridView.

Лош пример: Telerik_WinControlsGridView, Classes.

Имена на асемблита

Имената на асемблитата съвпадат с името на основния пакет. Добри примери са:

-     Telerik.WinControls.GridView.dll

-     Oracle.DataAccess.dll

-     Interop.CAPICOM.dll

Неправилни имена:

-     Telerik_WinControlsGridView.dll

-     OracleDataAccess.dll

Имена на методи

В имената на методите отново всяка отделна дума трябва да е с главна буква – PascalCase.

Имената на методите трябва да се съставят по схемата <глагол> + <обект>, например PrintReport(),  LoadSettings() или SetUserName(). Обектът може да е съществително или да е съставен от съществително и прилагателно, например ShowAnswer(), ConnectToRandomTorrentServer() или FindMaxValue().

Името на метода трябва да отговаря на въпроса какво извършва метода. Ако не можете да измислите добро име, вероятно трябва да преразгледате самия метод и дали е удачно написан.

Като примери за лоши имена на методи можем да дадем следните: DoWork() (не става ясно каква точно работа върши), Printer() (няма глагол), Find2() (ами защо не е Find7()?), ChkErr() (не се препоръчват съкращения), NextPosition() (няма глагол).

Понякога единични глаголи са също добро име за метод, стига да става ясно какво прави съответния метод и върху какви обекти оперира. Например ако имаме клас Task, методите Start(), Stop() и Cancel() са с добри имена, защото става ясно, че стартират, спират или оттеглят изпълнението на задачата, в текущия обект (this). В други случаи единичния глагол е грешно име, примерно в клас с име Utils методи с имена Evaluate(), Create() или Stop() са неадекватни, защото няма контекст.

Методи, които връщат стойност

Имената на методите, които връщат стойност, трябва да описват връщаната стойност, например GetNumberOfProcessors(), FindMinPath(), GetPrice(), GetRowsCount(), CreateNewInstance().

Примери за лоши имена на методи, които връщат стойност (функции) са следните: ShowReport() (не става ясно какво връща методът), Value() (трябва да е GetValue() или HasValue()), Student() (няма глагол), Empty() (трябва да е IsEmpty()).

Когато се връща стойност трябва да я ясна мерната единица: MeasureFontInPixels(...), а не MeasureFont(...).

Единствена цел на методите

Един метод, който извършва няколко неща е трудно да бъде именуван – какво име ще дадете на метод, който прави годишен отчет на приходите, сваля обновления на софтуера от интернет и сканира системата за вируси? Например Create­Annual­Incomes­Report­Download­Updates­And­Scan­For­Viruses?

clip_image001[5]

Методите трябва да имат една единствена цел, т.е. да решават само една задача, не няколко едновременно!

Методи с няколко цели (weak cohesion) не могат и не трябва да се именуват правилно. Те трябва да се преработят.

Свързаност на отговорностите и именуване

Името трябва да описва всичко, което методът извършва. Ако не може да се намери подходящо име, значи няма силна свързаност на отговор­ностите (strong cohesion), т.е. методът върши много неща едновременно и трябва да се раздели на няколко отделни метода.

Ето един пример: имаме метод, който праща e-mail, печата отчет на принтер и изчислява разстояние между точки в тримерното евклидово пространство. Какво име ще му дадем? Може би ще го кръстим SendEmailAndPrintReportAndCalc3DDistance()? Очевидно е, че нещо не е наред с този метод – трябва да преработим кода вместо да се мъчим да дадем добро име. Още по-лошо е, ако дадем грешно име, примерно SendEmail(). Така подвеждаме всички останали програмисти, че този метод праща поща, а той всъщност прави много други неща.

clip_image001[6]

Даването на заблуждаващо име за метод е по-лошо дори от това да го кръстим method1(). Например ако един метод изчислява косинус, а ние му дадем за име sqrt(), ще си навлечем яростта на всички колеги, които се опитват да ползват нашия код.

Колко да са дълги имената на методите?

Тук важат същите препоръки като за класовете – не трябва да се съкращава, ако не е ясно.

Добри примери са имената: LoadCustomerSupportNotificationService(), CreateMonthlyAndAnnualIncomesReport().

Лоши примери са LoadCustSuppSrvc(), CreateMonthIncReport().

Параметри на методите

Параметрите имат следния вид: [Съществително] или [Прилагателно]+ [Съществително]. Всяка дума от името трябва да е с главна буква, с изключение на първата, тази нотация се нарича camelCase. Както и при всеки друг елемент от кода и тук именуването трябва да е смислено и да носи полезна информация.

Добри примери: firstName, report, usersList, fontSizeInPixels, speedKmH, font.

Лоши примери: p, p1, p2, populate, LastName, last_name, convertImage.

Имена на свойства

Имената на свойствата са нещо средно между имената на методите и на променливите – започват с главна буква (PascalCase), но нямат глагол (като променливите). Името им се състои от (прилагателно+) съществи­телно.

Ако имаме свойство X е недобра практика да имаме и метод GetX() – ще бъде объркващо.

Ако свойството е енумерация, можете да се замислите дали да не кръстите свойството на самата енумерация. Например ако имаме енумерация с име CacheLevel, то и свойството може да се кръсти CacheLevel.

Имена на променливи

Имената на променливите (променливи използвани в метод) и член-променливите (променливи използвани в клас) според Microsoft конвенцията трябва да спазват camelCase нотацията.

Променливите трябва да имат добро име като всички други елементи на кода. Добро име е такова, което ясно и точно описва обекта, който променливата съдържа. Например добри имена на променливи са account, blockSize и customerDiscount. Лоши имена са: r18pq, __hip, rcfd, val1, val2.

Името трябва да адресира проблема, който решава променливата. Тя трябва да отговаря на въпроса "какво", а не "как". В този смисъл добри имена са employeeSalary, employees. Лоши имена са, несвързаните с решавания проблем имена myArray, customerFile, customerHashTable.

clip_image001[7]

Предпочитайте имена от бизнес домейна, в който ще оперира софтуера – CompanyNames срещу StringArray.

Оптималната дължина на името на променлива е от 10 до 16 символа. Изборът на дължината на името зависи от обхвата – променливите с по-голям обхват и по-дълъг живот имат по-дълго и описателно име:

protected Account[] customerAccounts;

Променливите с малък обхват и кратък живот могат да са по-кратки:

for (int i=0; i < customers.Length; i++) { … }

Имената на променливите трябва да са разбираеми без предварителна подготовка. Поради тази причина не е добра идея да се премахват гласните от името на променливата с цел съкращение – btnDfltSvRzlts не е много разбираемо име.

Най-важното е, че каквито и правила да бъдат изградени за именуване на променливите, те трябва да бъдат консистентно прилагани навсякъде из кода, в рамките на всички модули на целия проект и от всички членове на екипа. Неконсистентно прилаганото правило е по-опасно от липсата на правило въобще.

Имена на булеви елементи

Параметрите, свойствата и променливите могат да бъдат от булев тип. В тази точка ще опишем спецификата на този тип елементи.

Имената им трябва да дават предпоставка за истина или лъжа. Например: canRead, available, isOpen, valid. Примери за неадекватни имена на булеви променливи са: student, read, reader.

Би било полезно булевите елементи да започват с is, has или can (с големи букви за свойствата), но само ако това добавя яснота.

Не трябва да се използват отрицания (предполагат префикса not), защото се получават следните странности:

if (! notFound) { … }

Добри примери: hasPendingPayment, customerFound, validAddress, positiveBalance, isPrime.

Лоши примери: notFound, run, programStop, player, list, findCustomerById, isUnsuccessfull.

Имена на константи

В C# константите са статични непроменими променливи и се дефинират по следния начин:

public struct Int32

{

      public const int MaxValue = 2147483647;

}

Имената на константите трябва да се изписват изцяло с главни букви с долна черта между думите. Пример:

public static class Math

{

      public const double PI = 3.14159;

}

Имената на константите точно и ясно трябва да описват смисъла на даде­ното число, стринг или друга стойност, а не самата стойност. Например, ако една константа се казва number314159, тя е безполезна.

Именуване на специфични типове данни

Имената на променливи, използвани за броячи, е хубаво да включват в името си дума, която указва това, например usersCount, rolesCount, filesCount.

Променливи, които се използват за описване на състояние на даден обект, трябва да бъдат именувани подходящо. Ето няколко примера: threadState, transactionState.

Временните променливи най-често са с безлични имена (което указва, че са временни променливи, т.е. имат много кратък живот). Добри примери са index, value, count. Неподходящи имена са a, aa, tmpvar1, tmpvar2.

Именуване с префикси или суфикси

В по-старите езици (например C) съществуват префиксни или суфиксни нотации за именуване. Много популярна в продължение на много години е била Унгарската нотация. Унгарската нотация е префиксна конвенция за именуване, чрез която всяка променлива получава префикс, който обоз­начава типа й или предназначението й. Например в Win32 API името lpcstrUserName би означавало променлива, която представлява указател към масив от символи, който завършва с 0 и се интерпретира като стринг.

В .NET подобни конвенции не са придобили популярност, защото средите за разработка показват типа на всяка променлива. Изключение донякъде правят графични библиотеки.

Форматиране на кода

Форматирането, заедно с именуването, е едно от основните изисквания за четим код. Без форматиране, каквито и правила да спазваме за имената и структурирането на кода, кодът няма да се чете лесно.

Целите на форматирането са две – по-лесно четене на кода и (следствието от първата цел) по-лесно поддържане на кода. Ако форматирането прави кода по-труден за четене, значи не е добро. Всяко форматиране (отместване, празни редове, подреждане, подравняване и т.н.) може да донесе както ползи, така и вреди. Важно е форматирането на кода да следва логическата структура на програмата, така че да подпомага четенето и логическото й разбиране.

clip_image001[8]

Форматирането на програмата трябва да разкрива него­вата логическа структура. Всички правила за форматира­не на кода имат една и съща цел – подобряване на четимостта на кода чрез разкриване на логическата му структура.

В средите за разработка на Microsoft кодът може да се форматира автоматично с клавишната комби­нация [Ctrl+K, Ctrl+F]. Могат да бъдат зададени различни стандарти за форматиране на код – Microsoft конвенцията, както и потребителски дефинирани стандарти.

Сега ще разгледаме правилата за форматиране от код-конвенцията на Microsoft за C#.

Защо кодът има нужда от форматиране?

public   const    string                    FILE_NAME

="example.bin"  ;  static void Main   (             ){

FileStream   fs=     new FileStream(FILE_NAME,FileMode

.   CreateNew)   // Create the writer      for data  .

;BinaryWriter w=new BinaryWriter     (    fs      );//

Write data to                               Test.data.

for(  int i=0;i<11;i++){w.Write((int)i);}w   .Close();

fs   .   Close  (  ) // Create the reader    for data.

;fs=new FileStream(FILE_NAME,FileMode.            Open

,  FileAccess.Read)     ;BinaryReader                r

= new BinaryReader(fs);  // Read data from  Test.data.

 for (int i = 0; i < 11; i++){ Console      .WriteLine

(r.ReadInt32                                       ())

;}r       .    Close   (   );  fs .  Close  (  )  ;  }

Може би този код е достатъчен като отговор?

Форматиране на блокове

Блоковете се заграждат с { и }. Те трябва да са на отделни редове. Съдържанието на блока трябва да е изместено навътре с една табулация:

if ( some condition )

{

  // Block contents indented by a single [Tab]

  // Don't use spaces for indentation

}

Това правило важи за пакети, класове, методи, условни конструкции, цикли и т.н.

Вложените блокове се отместват допълнително. Тук тялото на класа е отместено от тялото на пакета, тялото на метода е отместено допълни­телно, както и съдържанието на условната конструкция:

namespace Chapter_21_Quality_Code

{

      public class IndentationExample

      {

            private int Zero()

            {

                  if (true)

                  {

                        return 0;

                  }

            }

      }

}

Правила за форматиране на метод

Съгласно конвенцията за писане на код, препоръчана от Microsoft, е добре да се спазват някои правила за форматиране на кода, при декларирането на методи.

Форматиране на множество декларации на методи

Когато в един клас имаме повече от един метод, трябва да разделяме декларациите им с един празен ред:

IndentationExample.cs

public class IndentationExample

{

 

      public static void DoSth1()

      {

            // ...

      }// Follows one blank line

 

      public static void DoSth2()

      {

            // ...

      }

}

Как да поставяме кръгли скоби?

В конвенцията за писане на код, на Microsoft, се препоръчва, между ключова дума, като например – for, while, if, switch... и отваряща скоба да поставяме интервал:

while (!EOF)

{

      // ... Code ...

}

Това се прави с цел да се различават по-лесно ключовите думи.

При имената на методите не се оставя празно място преди отварящата кръгла скоба.

public void CalculateCircumference(int radius)

{

      return 2 * Math.PI * radius;

}

В този ред на мисли, между името на метода и отварящата кръгла скоба – "(", не трябва да има невидими символи (интервал, табулация и т.н.):

public static void PrintLogo()

{

      // ... Code ...

}

Форматиране на списъка с параметри на методи

Когато имаме метод с много параметри, трябва добре да оставяме един интер­вал разстояние между поредната запетайка и типа на следващия параме­тър, но не и преди запетаята:

public void CalcDistance(Point startPoint, Point endPoint)

Съответно, същото правило прилагаме, когато извикваме метод с повече от един параметър. Преди аргументите, предшествани от запетайка, пос­тавяме интервал:

DoSmth(1, 2, 3);

Правила за форматирането на типове

Когато създаваме класове, интерфейси, структури или енумерации също е добре да следваме няколко препоръки от Microsoft за форматиране на кода в класовете.

Правила за подредбата на съдържанието на класа

Както знаем, на първия ред се декларира името на класа, предхождано от ключовата дума class:

public class Dog

{

След това се декларират константите, като първо се декларират тези с модификатор за достъп public, след това тези с protected и накрая – с private:

      // Static variables

      public const string SPECIES = "Canis Lupus Familiaris";

След тях се декларират и нестатичните полета. По подобие на статичните, първо се декларират тези с модификатор за достъп public, след това тези с protected и накрая – тези с private:

      // Instance variables

      private int age;

След нестатичните полета на класа, идва ред на декларацията на кон­структорите:

      // Constructors

      public Dog(string name, int age)

      {

            this.Name = name;

            this.age = age;

      }

След конструкторите се декларират свойствата:

      // Properties

      public string Name { get; set; }

Най-накрая, след свойствата, се декларират методите на класа. Препоръчва се да групираме методите по функционалност, вместо по ниво на достъп или област на действие. Например, метод с модификатор за достъп private, може да бъде между два метода с модификатори за достъп – public. Целта на всичко това е да се улесни четенето и разбира­нето на кода. Завършваме със скоба за край на класа:

      // Methods

      public void Breath()

      {

            // TODO: breathing process

      }

      public void Bark()

      {

            Console.WriteLine("wow-wow");

      }

}

Правила за форматирането на цикли и условни конструкции

Форматирането на цикли и условни конструкции става по правилата за форматиране на методи и класове. Тялото на условна конструкция или цикъл задължително се поставя в блок, започващ с "{" и завършващ със "}". Първата скоба се поставя на нов ред, веднага след условието на цикъла или условната конструкция. Тялото на цикъл или условна конструкция задължително се отмества надясно с една табулация. Ако условието е дълго и не се събира на един ред, се пренася на нов ред с две табулации надясно. Ето пример за коректно форматирани цикъл и условна конструкция:

public static void Main()

{

      Dictionary<int, string> bulgarianNumbersHashtable =

            new Dictionary<int, string>();

      bulgarianNumbersHashtable.Add(1, "едно");

      bulgarianNumbersHashtable.Add(2, "две");

      bulgarianNumbersHashtable.Add(3, "три");

 

      foreach (KeyValuePair<int, string> pair in

            bulgarianNumbersHashtable.ToArray())

      {

            Console.WriteLine("Pair: [{0},{1}]", pair.Key, pair.Value);

      }

}

Изключително грешно е да се използва отместване от края на условието на цикъла или условната конструкция като в този пример:

foreach (Student s in students) {

                              Console.WriteLine(s.Name);

                              Console.WriteLine(s.Age);

                     }

Използване на празни редове

Типично за начинаещите програмисти е да поставят безразборно в прог­рамата си празни редове. Наистина, празните редове не пречат, защо да не ги поставяме, където си искаме и защо да ги чистим, ако няма нужда от тях? Причината е много проста: празните редове се използват за разде­ляне на части от програмата, които не са логическо свързани – празните редове са като начало и край на параграф. Празни редове се поставят за разделяне на методите един от друг, за отделяне на група член-променливи от друга група член-променливи, които имат друга логическа задача, за отделяне на група програмни конструкции от друга група програмни конструкции, които представляват две отделни части на програмата.

Ето един пример с два метода, в който празните редове не са използвани правилно и това затруднява четимостта на кода:

public static void PrintList(IList<int> list)

{

      Console.Write("{ ");

      foreach (int item in list)

      {

            Console.Write(item);

 

            Console.Write(" ");

 

           

      }

      Console.WriteLine("}");

}

public static void Main()

{

      IList<int> firstList = new List<int>();

      firstList.Add(1);

 

      firstList.Add(2);

      firstList.Add(3);

      firstList.Add(4);

      firstList.Add(5);

      Console.Write("firstList = ");

      PrintList(firstList);

      List<int> secondList = new List<int>();

      secondList.Add(2);

 

      secondList.Add(4);

      secondList.Add(6);

      Console.Write("secondList = ");

      PrintList(secondList);

      List<int> unionList = new List<int>();

      unionList.AddRange(firstList);

      Console.Write("union = ");

 

      PrintList(unionList);

}

Сами виждате, че празните редове не показват логическата структура на програмата, с което нарушават основното правило за форматиране на кода. Ако преработим програмата, така че да използваме правилно праз­ните редове за отделяне на логически самостоятелните части една от друга, ще получим много по-лесно четим код:

public static void PrintList(IList<int> list)

{

      Console.Write("{ ");

      foreach (int item in list)

      {

            Console.Write(item);

            Console.Write(" ");

      }

      Console.WriteLine("}");

}

 

public static void Main()

{

      IList<int> firstList = new List<int>();

      firstList.Add(1);

      firstList.Add(2);

      firstList.Add(3);

      firstList.Add(4);

      firstList.Add(5);

      Console.Write("firstList = ");

      PrintList(firstList);

 

      List<int> secondList = new List<int>();

      secondList.Add(2);

      secondList.Add(4);

      secondList.Add(6);

      Console.Write("secondList = ");

      PrintList(secondList);

 

      List<int> unionList = new List<int>();

      unionList.AddRange(firstList);

      Console.Write("union = ");

      PrintList(unionList);

}

Правила за пренасяне и подравняване

Когато даден ред е дълъг, разделете го на два или повече реда, като редовете след първия отместете надясно с една табулация:

Dictionary<int, string> bulgarianNumbersHashtable =

      new Dictionary<int, string>();

Грешно е да подравнявате сходни конструкции спрямо най-дългата от тях, тъй като това затруднява поддръжката на кода:

DateTime                      date              = DateTime.Now.Date;

int                                 count       = 0;

Student                            student           = new Strudent();

List<Student>           students    = new List<Student>();

Или

matrix[x, y]                                                      == 0;

matrix[x + 1, y + 1]                                  == 0;

matrix[2 * x + y, 2 * y + x]        == 0;

matrix[x * y, x * y]                                  == 0;

Грешно е да подравнявате параметрите при извикване на метод вдясно спрямо скобата за извикване:

Console.WriteLine("word '{0}' is seen {1} times in the text",

                  wordEntry.Key,

                  wordEntry.Value);

Същият код може да се форматира правилно по следния начин (този начин не е единственият правилен):

Console.WriteLine(

      "word '{0}' is seen {1} times in the text",

      wordEntry.Key,

      wordEntry.Value);

Висококачествени класове

Софтуерен дизайн

Когато се проектира една система, често отделните подзадачи се отделят в отделни модули или подсистеми. Задачите, които решават, трябва да са ясно дефинирани. Взаимовръзките между отделните модули също трябва да са ясни предварително, а не да се измислят в движение.

В предишната глава, в която разяснихме ООП, показахме как се използва обектно-ориентираното моделиране за дефиниране на класове от реалните актьори в домейна на решаваната задача. Там споменахме и употребата на шаблони за дизайн.

Добрият софтуерен дизайн е с минимална сложност и е лесен за разбиране. Поддържа се лесно и промените се правят праволинейно (вижте спагети кода в предходната глава). Всяка една единица (метод, клас, модул) е логически свързана вътрешно (strong cohesion), функ­ционално независима и минимално обвързана с други модули (loose coupling). Добре проектираният код се преизползва лесно.

ООП

При създаването на качествени класове основните правила произтичат от четирите принципа на ООП:

Абстракция

Няколко основни правила:

-      Едно и също ниво на абстракция при публични членове на класа.

-      Интерфейсът на класа трябва да е изчистен и ясен.

-      Класът описва само едно нещо.

-      Класът трябва да скрива вътрешната си имплементация.

Кодът се развива във времето. Важно е въпреки еволюцията на класовете, техните интерфейси да не се развалят, например:

class Employee

{

  public string firstName;

  public string lastName;

  ...

  public SqlCommand FindByPrimaryKeySqlCommand(int id);

}

Последният метод е несъвместим с нивото на абстракция, на което работи Employee. Потребителят на класа не трябва да знае въобще, че той работи с база от данни вътрешно.

Наследяване

Не скривайте методи в класовете наследници:

public class Timer

{

      public void Start() {...}

}

public class AtomTimer : Timer

{

      public void Start() {...}

}

Методът в класа-наследник скрива реалната имплементация. Това не е препоръчително. Ако все пак това поведение е желано (в редките случаи, в които това се налага), се използва ключовата дума new.

Преместете общи методи, данни, поведение колкото се може по-нагоре в дървото на наследяване. Така тази функционалност няма да се дублира и ще бъде достъпна от по-голяма аудитория.

Ако имате клас, който има само един наследник, смятайте това за съмнително. Това ниво на абстракция може би е излишно. Съмнителен би бил и метод, който пренаписва такъв от базовия клас, който обаче не прави нищо повече от базовия метод.

Дълбокото наследяване с повече от 6 нива е трудно за проследяване и поддържане, затова не е препоръчително. В наследен клас достъпвайте член-променливите през свойства, а не директно.

Следният пример демонстрира кога трябва се предпочете наследяване пред проверка на типовете:

switch (shape.Type)

{

  case Shape.Circle:

    shape.DrawCircle();

    break;

  case Shape.Square:

    shape.DrawSquare();

    break;

  ...

}

Тук подходящо би било Shape да бъде наследено от Circle и Square, които да имплементират виртуалния метод Shape.Draw().

Капсулация

Добър подход е всички членове да бъдат първо private. Само тези, които е нужно да се виждат навън, се променят първо на protected и после на public.

Имплементационните детайли трябва да са скрити. Ползвателите на един качествен клас, не трябва да знаят как той работи вътрешно, за тях трябва да е ясно какво прави той и как се използва.

Член-променливите трябва да са скрити зад свойства. Публичните член-променливи са проява на некачествен код. Константите са изключение.

Публичните членове на един клас трябва да са последователни спрямо абстракцията, която представя този клас. Не правете предположения как ще се използва един клас.

clip_image001[9]

Не разчитайте на недокументирана вътрешна имплемента­ционна логика.

Конструктори

За предпочитане е всички членове на класа да са инициализирани в конструктора. Опасно е използването на неинициализиран клас. Полуинициализиран клас е още по-опасно. Инициализирайте член-променливите в реда, в който са декларирани.

Дълбоко копие на един клас е копие, в което всички член-променливи се копират, и техните член-променливи също се копират. Плитко копие е такова, в което се копират само членовете на първо ниво.

Плитко копие:

clip_image003

Дълбоко копие:

clip_image005

Плитките копия са опасни, защото промяната в един обект води до скрити промени в други. Забележете как във втория пример промяната на възрастта на Ирен в оригинала не води до промяна на възрастта на Ирен в копието. При плитките копия промяната ще се отрази и на двете места.

Висококачествени методи

Качеството на нашите методи е от съществено значение за създаването на висококачествен софтуер и неговата поддръжка. Те правят програмите ни по-четливи и по-разбираеми. Методите ни помагат да намалим сложността на софтуера, да го направим по-гъвкав и по-лесен за модифициране.

От нас зависи, до каква степен ще се възползваме от тези предимства. Колкото по-високо е качеството на методите ни, толкова повече печелим от тяхната употреба. В следващите параграфи ще се запознаем с някои от основните принципи за създаване на качествени методи.

Защо да използваме методи?

Преди да започнем да говорим за добрите имена на методите, нека отделим известно време и да обобщим причините, поради които изпол­зваме методи.

Методът решава по-малък проблем. Много методи решават много малки проблеми. Събрани заедно, те решават по-голям проблем – това е римското правило "разделяй и владей" – по-малките проблеми се решават по-лесно.

Чрез методите се намалява сложността на задачата – сложните проб­леми се разбиват на по-прости, добавя се допълнително ниво на абстракция, скриват се детайли за имплементацията и се намалява рискът от неуспех. С помощта на методите се избягва повторението на еднакъв код. Скриват се сложни последователности от действия.

Най-голямото предимство на методите е възможността за преизползване на код – те са най-малката преизползваема единица код. Всъщност точно така са възникнали методите.

Какво трябва да прави един метод?

Един метод трябва да върши работата, която е описана в името му и нищо повече. Ако един метод не върши това, което предполага името му, то или името му е грешно, или методът върши много неща едновременно, или просто методът е реализиран некоректно. И в трите случая методът не отговаря на изискванията за качествен програ­мен код и има нужда от преработка.

Един метод или трябва да свърши работата, която се очаква от него, или трябва да съобщи за грешка. В .NET съобщаването за грешки се осъще­ствява с хвърляне на изключение. При грешни входни данни е недопус­тимо даден метод да връща грешен резултат. Методът или трябва да работи коректно или да съобщи, че не може да свърши работата си, защото не са на лице необходимите му условия (при некоректни пара­метри, неочаквано състояние на обектите и др.).

Например ако имаме метод, който прочита съдържанието на даден файл, той трябва да се казва ReadFileContents() и трябва да връща byte[] или string (в зависимост дали говорим за двоичен или текстов файл). Ако файлът не съществува или не може да бъде отворен по някаква причина, методът трябва да хвърли изключение, а не да върне празен низ или null. Връщането на неутрална стойност (например null) вместо съобще­ние за грешка не е препоръчителна практика, защото извикващият метод няма възможност да обработи грешката и изгубва носещото богата информация изключение.

clip_image001[10]

Един публичен метод или трябва да върши коректно точно това, което предполага името му, или трябва да съобщава за грешка. Всякакво друго поведение е некоректно.

Описаното правило има някои изключения. Обикновено то се прилага най-вече за публичните методи в класа. Те или трябва да работят коректно, или трябва да съобщят за грешка. При скритите (private) методи може да се направи компромис - да не се проверява за некоректни параметри, тъй като тези методи може да ги извика само авторът на класа, а той би трябвало добре знае какво подава като параметри и не винаги трябва да обработва изключителните ситуации, защото може да ги предвиди. Но не забравяйте – това е компромис.

Ето два примера за качествени методи:

long Sum(int[] elements)

{

      long sum = 0;

      foreach (int element in elements)

      {

            sum = sum + element;

      }

      return sum;

}

 

double CalcTriangleArea(double a, double b, double c)

{

      if (a <= 0 || b <= 0 || c <= 0)

      {

            throw new ArgumentException("Sides should be positive.");

      }

      double s = (a + b + c) / 2;

      double area = Math.Sqrt(s * (s - a) * (s - b) * (s - c));

      return area;

}

Strong Cohesion и Loose Coupling

Правилата за логическа свързаност на отговорностите (strong cohesion) и за функционална независимост и минимална обвързаност с останалите методи и класове (loose coupling) важат с пълна сила за методите.

Вече обяснихме, че един метод трябва да решава един проблем, не няколко. Един метод не трябва да има странични ефекти или да решава няколко несвързани задачи, защото няма да можем да му дадем подхо­дящо име, което пълно и точно го описва. Това означава, че всички методи, които пишем, трябва да имат strong cohesion, т.е. да са насочени към решаването на една единствена задача.

Методите трябва минимално да зависят от останалите методи и от класа, в който се намират и от останалите класове. Това свойство се нарича loose coupling.

В идеалния случай даден метод трябва да зависи единствено от парамет­рите си и да не използва никакви други данни като вход или като изход. Такива методи лесно могат да се извадят и да се преизползват в друг проект, защото са независими от средата, в която се изпълняват.

Понякога методите зависят от private променливи в класа, в който са дефинирани или променят състоянието на обекта, към който принадлежат. Това не е грешно и е нормално. В такъв случай говорим за обвързване (coupling) между метода и класа. Такова обвързване не е проблемно, защото целият клас може да се извади и премести в друг проект и ще започне да работи без проблем. Повечето класове от Common Type System дефинират методи, които зависят единствено от данните в класа, който ги дефинира и от подадените им параметри. В стандартните библиотеки зависимостите на методите от външни класове са минимални и затова тези библиотеки са лесни за използване.

Ако даден метод чете или променя глобални данни или зависи от още 10 обекта, които трябва да се инициализирани в инстанцията на неговия клас, той е силно обвързан с всички тези обекти. Това означава, че функционира сложно и се влияе от прекалено много външни условия и следователно възможността за грешки е голяма. Методи, които разчитат на прекалено много външни зависимости, са трудни за четене, за разби­ране и за поддръжка. Силното функционално обвързване е лошо и трябва да се избягва, доколкото е възможно, защото води до код като спагети.

Сега погледнете същите два метода. Намирате ли грешки?

long Sum(int[] elements)

{

      long sum = 0;

      for (int i = 0; i < elements.Length; i++)

      {

            sum = sum + elements[i];

            elements[i] = 0; // Hidden side effect

      }

      return sum;

}

 

double CalcTriangleArea(double a, double b, double c)

{

      if (a <= 0 || b <= 0 || c <= 0)

      {

            return 0; // Incorrect result

      }

      double s = (a + b + c) / 2;

      double area = Math.Sqrt(s * (s - a) * (s - b) * (s - c));

      return area;

}

Колко дълъг трябва да е един метод?

През годините са правени различни изследвания за оптималната дължина на методите, но в крайна сметка универсална формула за дължина на даден метод не съществува.

Практиката показва, че като цяло трябва да предпочитаме по-кратки методи (не повече от един екран). Те са по-лесни за четене и разбиране, а вероятността да допуснем грешка при тях е значително по-малка.

Колкото по-голям е един метод, толкова по-сложен става той. Последващи модификации са значително по-трудни, отколкото при кратките методи и изискват много повече време. Тези фактори са предпоставка за допускане на грешки и по-трудна поддръжка.

Препоръчителната дължина на един метод е не-повече от един екран, но тази препоръка е само ориентировъчна. Ако методът се събира на екрана, той е по-лесен за четене, защото няма да се налага скролиране. Ако методът е по-дълъг от един екран, това трябва да ни накара да се замислим дали не можем да го разделим логически на няколко по-прости метода. Това не винаги е възможно да се направи по смислен начин, така че препоръката за дължината на методите е ориентировъчна.

Макар дългите методи да не са за предпочитане, това не трябва да е безусловна причина да разделяме на части даден метод само защото е дълъг. Методите трябва да са толкова дълги, колкото е необходимо.

clip_image001[11]

Силната логическа свързаност на отговорностите при ме­то­дите е много по-важна от дължината им.

Ако реализираме сложен алгоритъм и в последствие се получи дълъг ме­тод, който все пак прави едно нещо и го прави добре, то в този случай дъл­жината не е проблем.

Във всеки случай, винаги, когато даден метод стане прекалено дълъг, трябва да се замисляме, дали не е по-подходящо да изнесем част от кода в отделни методи, изпълняващи определени подзадачи.

Параметрите на методите

Едно от основните правила за подредба на параметрите на методите е основният или основните параметри да са първи. Пример:

public void Archive(PersonData person, bool persistent) {...}

Обратното би било доста по-объркващо:

public void Archive(bool persistent, PersonData person) {...}

Друго основно правило е имената на параметрите да са смислени. Честа грешка, е имената на параметрите да бъдат свързани с имената на типовете им. Пример:

public void Archive(PersonData personData) {...}

Вместо нищо незначещото име personData (което носи информация един­ствено за типа), можем да използваме по-добро име (така е доста по-ясно кой точно обект архивираме):

public void Archive(PersonData loggedUser) {...}

Ако има методи с подобни параметри, тяхната подредба трябва да е кон­систентна. Това би направило кода много по-лесен за четене:

public void Archive(PersonData person, bool persistent) {...}

 

public void Retrieve(PersonData person, bool persistent) {...}

Важно е да няма параметри, които не се използват. Те само могат да подведат ползвателя на този код.

Параметрите не трябва да се използват и като работни променливи – не трябва да се модифицират. Ако модифицирате параметрите на методите, кодът става по-труден за четене и логиката му – по-трудна за проследя­ване. Винаги можете да дефинирате нова променлива вместо да проме­няте параметър. Пестенето на памет не е оправдание в този сценарий.

Неочевидните допускания трябва да се документират. Например мерната единица при подаване на числа. Ако имаме метод, който изчис­лява косинус от даден ъгъл, трябва да документираме дали ъгълът е в градуси или в радиани, ако това не е очевидно.

Броят на параметрите не трябва да надвишава 7. Това е специално, магическо число. Доказано е, че човешкото съзнание не може да следи повече от около 7 неща едновременно. Разбира се, тази препоръка е само за ориентир. Понякога се налага да предавате и много повече параметри. В такъв случай се замислете дали не е по-добре да ги предавате като някакъв клас с много полета. Например ако имате метода AddStudent(…) с 15 параметъра (име, адрес, контакти и още много други), можете да намалите параметрите му като подавате групи логически свързани пара­метри като клас, примерно така: AddStudent(personalData, contacts, universityDetails). Всеки от новите 3 параметъра ще съдържа по няколко полета и пак ще се прехвърля същата информация, но в по-лесен за възприемане вид.

Понякога е логически по-издържано вместо един обект на метода да се подадат само едно или няколко негови полета. Това ще зависи най-вече от това дали методът трябва да знае за съществуването на този обект или не. Например имаме метод, който изчислява средния успех на даден студент – CalcAverageResults(Student s). Понеже успехът се изчислява от оценките на студента и останалите му данни нямат значение, е по-добре вместо Student да се предава като параметър списък от оценки. Така методът придобива вида CalcAverageResults(IList<Mark>).

Правилно използване на променливите

В настоящия параграф ще разгледаме няколко добри практики при локалната работа с променливи.

Връщане на резултат

Когато връщаме резултат от метод, той трябва да се запази в променлива преди да се върне. Следният пример не казва какво се връща като резултат:

return days * hoursPerDay * ratePerHour;

По-добре би било така:

int salary = days * hoursPerDay * ratePerHour;

return salary;

Има няколко причини да запазваме резултата преди да го видим. Едната е, че така документираме кода – по името на допълнителната променлива става ясно какво точно връщаме. Другата причина е, че когато дебъгваме програмата, ще можем да я спрем в момента, в който е изчислена връщаната стойност и ще можем да проверим дали е коректна. Третата причина е, че избягваме сложните изрази, които понякога може да са няколко реда дълги и заплетени.

Принципи при инициализиране

В .NET всички член-променливи в класовете се инициализират автома­тично още при декла­риране (за разлика от C/C++). Това се извършва от средата за изпълнение. Така се избягват грешки с неправилно инициализи­рана памет. Всички променливи, сочещи обекти (reference type variable) се инициализират с null, а всички примитивни типове – с 0 (false за bool).

Компилаторът задължава всички локални променливи в кода на една програма да бъдат инициализирани изрично преди употреба, иначе връща грешка при компилация. Ето един пример, който ще предизвика грешка при компилация, защото се прави опит за използване на неинициализи­рана променлива:

public static void Main()

{

      int value;

      Console.WriteLine(value);

}

При опит за компилация се връща грешка на втория ред:

clip_image007

Ето как изглеждат нещата в средата за разработка:

clip_image009

Ето още един малко по-сложен пример:

int value;

if (condition1)

{

      if (condition2)

      {

            value = 1;

      }

}

else

{

      value = 2;

}

Console.WriteLine(value);

За щастие компилаторът е достатъчно интелигентен и хваща подобни "недоразумения" – отново същата грешка.

Забележете следната особеност: ако сложим else на вложения if в горния код, всичко ще се компилира. Компилаторът проверява всички възможни пътища, по които може да мине изпълнението и ако при всеки един от тях има инициализация на променливата, той не връща грешка и променливата се инициализира правилно.

Добрата практика е всички променливи да се инициализират изрично още при деклариране:

int value = 0;

Student intern = null;

Частично-инициализирани обекти

Някои обекти, за да бъдат правилно инициализирани, трябва да имат стойности на поне няколко техни полета. Например обект от тип Човек, трябва да има стойност на полетата "име" и "фамилия". Това е проблем, от който компилаторът не може да ни опази.

Единият начин да бъде решен този проблем е да се премахне конструк­торът по подразбиране (конструкторът без параметри) и на негово място да се сложат един или няколко конструктора, които получават достатъчно данни (във формата на параметри) за правилното инициализиране на съответния обект. Точно това е идеята на такива конструктори.

Деклариране на променлива в блок/метод

Съгласно конвенцията за писане на код на .NET, една променлива трябва да се декларира в началото на блока или тялото на метода, в който се намира:

static int Archive()

{

      int result = 0;       // beginning of method body

      // .. Code ...

}

 

if (condition)

{

      int result = 0;     // beginning of an "if" block

      // .. Code ...

}

Изключение правят променливите, които се декларират в инициа­ли­зи­ращата част на for цикъла:

for (int i = 0; i < data.Length; i++) {...}

Повечето добри програмисти предпочитат да декларират една променлива максимално близо до мястото, на което тя ще бъде използвана и по този начин да намалят нейния живот (погледнете следващия параграф) и същевременно възможността за грешка.

Обхват, живот, активност

Понятието обхват на променлива (variable scope) всъщност описва колко "известна" е една променлива. В .NET тя може да бъде (подредени в низходящ ред) статична променлива, член-променлива (на клас) и ло­кална променлива (в метод).

Колкото по-голям е обхватът на дадена променлива, толкова по-голяма е възможността някой да се обвърже с нея и така да увеличи своя coupling, което не е хубаво. Следователно обхватът на променливите трябва да е въз­можно най-малък.

Добър подход при работата с променливи е първоначално те да са с мини­мален обхват. При необходимост той да се разширява. Така по естествен начин всяка променлива получава необходимия за работата й обхват. Ако не знаете какъв обхват да ползвате, започвайте от private и при нужда преминавайте към protected или public.

Статичните променливи е най-добре да са винаги private и достъпът до тях да става контролирано, чрез извикване на подходящи методи.

Ето един пример за лошо семантично обвързване със статична промен­лива – ужасно лоша практика:

public class Globals

{

      public static int state = 0;

}

 

public class Genious

{

      public static void PrintSomething()

      {

            if (Globals.state == 0)

            {

                  Console.WriteLine("Hello.");

            }

            else

            {

                  Console.WriteLine("Good bye.");

            }

      }

}

Ако променливата state беше дефинирана като private, такова обвърз­ване нямаше да може да се направи, поне не директно.

Диапазон на активност (span) е средният брой линии между обръще­нията към дадена променлива. Той зависи от гъстотата на редовете код, в които тази променлива се използва. Диапазонът на променливите трябва да е минимален. По тази причина променливите трябва да се декларират и инициализират възможно най-близко до мястото на първата им упо­треба, а не в началото на даден метод или блок.

Живот (lifetime) на една променлива е обемът на кода от първото до последното й рефериране в даден метод. В тази дефиниция имаме предвид само локални променливи, понеже член-променливите живеят докато съществува класът, в който са дефинирани, а статичните промен­ливи – докато съществува виртуалната машина.

Ето един пример за неправилно използване на променливи (излишно голям диапазон на активност):

int count;

int[] numbers = new int[100];

 

for (int i = 0; i < numbers.Length; i++)

{

      numbers[i] = i;

}

count = 0;

 

for (int i = 0; i < numbers.Length / 2; i++)

{

      numbers[i] = numbers[i] * numbers[i];

}

 

for (int i = 0; i < numbers.Length; i++)

{

      if (numbers[i] % 3 == 0)

      {

            count++;

      }

}

 

Console.WriteLine(count);

clip_image010

 

 

 

 

 

  lifetime = 23 lines

   span = 23 / 4 = 5.75

В този пример променливата count служи за преброяване на числата, които се делят без остатък на 3 и се използва само в последния for цикъл. Тя е дефинирана излишно рано и се инициализира много преди да има нужда от инициализацията. Ако трябва да се преработи този код, за да се намали диапазонът на активност на променливата count, той ще добие следния вид:

int[] numbers = new int[100];

for (int i = 0; i < numbers.Length; i++)

{

      numbers[i] = i;

}

 

for (int i = 0; i < numbers.Length / 2; i++)

{

      numbers[i] = numbers[i] * numbers[i];

}

 

int count = 0;

for (int i = 0; i < numbers.Length; i++)

{

      if (numbers[i] % 3 == 0)

      {

            count++;

      }

}

 

Console.WriteLine(count);

clip_image011

 

 

 

 

 

  lifetime = 10 lines

   span = 10 / 3 = 3.33

Важно е програмистът да следи къде се използва дадена променлива, нейният диапазон на активност и период на живот. Основното правило е да се направят обхватът, животът и активността на променливите колкото се може по-малки. От това следва едно важно правило:

clip_image001[12]

Декларирайте локалните променливи възможно най-къс­но, непосредствено преди да ги използвате за първи път, и ги инициализирайте заедно с деклара­цията им.

Променливите с по-голям обхват и по-дълъг живот, трябва да имат по-описателни имена, примерно totalStudentsCount. Причината е, че те ще бъдат използвани на повече места и за по-дълго време и за какво служат няма да бъде ясно от контекста. Променливите с живот няколко реда могат да бъдат с кратко и просто име, примерно count. Те нямат нужда от дълги и описателни имена, защото техният смисъл е ясен от контекста, в който се използват, а този контекст е твърде малък (няколко реда), за да има двусмислия.

Работа с променливи – още правила

Една променлива трябва да се използва само за една цел. Това е много важно правило. Извиненията, че ако се преизползва едно променлива за няколко цели се пести на памет, в общия случай не са добро оправдание. Ако една променлива се ползва за няколко съвсем различни цели, какво име ще й дадем? Например, ако една променлива се използва да брои студенти и в някои случаи техните оценки, то как ще я кръстим: count, studentsCount, marksCount или StudentsOrMarksCount?

clip_image001[13]

Ползвайте една променлива само за една единствена цел. Иначе няма да можете да й дадете подходящо име.

Никога не трябва да има променливи, които не се използват. В такъв случай тяхното дефиниране е било безсмислено. За щастие сериозните среди за разработка издават предупреждение за подобни "нередности".

Трябва да се избягват и променливи със скрито значение. Например Пешо е оставил променливата Х, за да бъде видяна от Митко, който трябва да се сети да имплементира още един метод, в който ще я ползва.

Правилно използване на изрази

При работата с изрази има едно много просто правило: не ползвайте сложни изрази! Сложен израз наричаме всеки израз, който извършва повече от едно действие. Ето пример за сложен израз:

for (int i = 0; i < xCoord.Length; i++)

{

      for (int j = 0; j < yCoord.Length; j++)

      {

            matrix[i][j] =

                  matrix[xCoord[FindMax(i) + 1]][yCoord[FindMin(i) + 1]] *

                  matrix[yCoord[FindMax(i) + 1]][xCoord[FindMin(i) + 1]];

      }

}

В примерния код имаме сложно изчисление, което запълва дадена матрица спрямо някакви изчисления върху някакви координати. Всъщност е много трудно да се каже какво точно се случва, защото е използван сложен израз.

Има много причини, заради които трябва да избягваме използването на сложни изрази като в примера по-горе. Ще изброим някои от тях:

-     Кодът трудно се чете. В нашия пример няма да ни е лесно да разберем какво прави този код и дали е коректен.

-     Кодът трудно се поддържа. Помислете, какво ще ни струва да поправим грешка в този код, ако не работи коректно.

-     Кодът трудно се поправя, ако има дефекти. Ако примерният код по-горе даде IndexOutOfRangeException, как ще разберем извън границите на кой точно масив сме излезли? Това може да е масивът xCoord или yCoord или matrix, а излизането извън тези масиви може да е на няколко места.

-     Кодът трудно се дебъгва. Ако намерим грешка, как ще дебъгнем изпълнението на този израз, за да намерим грешката?

Всички тези причини ни подсказват, че писането на сложни изрази е вредно и трябва да се избягва. Вместо един сложен израз можем да напишем няколко по-прости изрази и да ги запишем в променливи с разумни имена. По този начин кодът става по-прост, по-ясен, по-лесен за четене и разбиране, по-лесен за промяна, по-лесен за дебъгване и по-лесен за поправяне. Нека сега пренапишем горния код, без да използваме сложни изрази:

for (int i = 0; i < xCoord.Length; i++)

{

      for (int j = 0; j < yCoord.Length; j++)

      {

            int maxStartIndex = FindMax(i) + 1;

            int minStartIndex = FindMax(i) - 1;

            int minXcoord = xCoord[minStartIndex];

            int maxXcoord = xCoord[maxStartIndex];

            int minYcoord = yCoord[minStartIndex];

            int maxYcoord = yCoord[maxStartIndex];

            matrix[i][j] =

                  matrix[maxXcoord][minYcoord] *

                  matrix[maxYcoord][minXcoord];

      }

}

Забележете колко по-прост и ясен стана кода. Наистина, без да знаем какво точно изчисление извършва този код, ще ни е трудно да го разберем, но ако настъпи изключение, лесно ще намерим на кой ред възниква и чрез дебъгера можем да проследим защо се получава и евентуално да го поправим.

clip_image001[14]

Не пишете сложни изрази. На един ред трябва да се извършва по една операция. Иначе кодът става труден за четене, за поддръжка, за дебъгване и за промяна.

Използване на константи

В добре написания програмен код не трябва да има "магически числа" и стрингове. Такива наричаме всички литерали в програмата, които имат стойност, различно от 0, 1, -1, "" и null (с дребни изключения).

За да обясним по-добре концепцията за използване на именувани кон­станти, ще дадем един пример за код, който има нужда от преработка:

public class GeometryUtils

{

      public static double CalcCircleArea(double radius)

      {

            double area = 3.14159206 * radius * radius;

            return area;

      }

 

      public static double CalcCirclePerimeter(double radius)

      {

            double perimeter = 6.28318412 * radius;

            return perimeter;

      }

 

      public static double CalcElipseArea(double axis1, double axis2)

      {

            double area = 3.14159206 * axis1 * axis2;

            return area;

      }

}

В примера използваме три пъти числото 3.14159206 (∏), което е повто­рение на код. Ако решим да променим това число, като го запишем например с по-голяма точност, ще трябва да променим програ­мата на три места. Възниква идеята да дефинираме това число като стойност, която е глобална за програмата и не може да се променя. Именно такива стой­ности в .NET се декларират като именувани константи по следния начин:

public const double PI = 3.14159206;

След тази декларация константата PI е достъпна от цялата програма и може да се ползва многократно. При нужда от промяна променяме само на едно място и промените се отразяват навсякъде. Ето как изглежда нашия примерен клас GeometryUtils след изнасянето на числото 3.14159206 в кон­станта:

public class GeometryUtils

{

      public const double PI = 3.14159206;

 

      public static double CalcCircleArea(double radius)

      {

            double area = PI * radius * radius;

            return area;

      }

 

      public static double CalcCirclePerimeter(double radius)

      {

            double perimeter = 2 * PI * radius;

            return perimeter;

      }

 

      public static double CalcElipseArea(

            double axis1, double axis2)

      {

            double area = PI * axis1 * axis2;

            return area;

      }

}

Кога да използваме константи?

Използването на константи помага да избегнем използването на "маги­чески числа" и стрингове в нашите програми и позволява да дадем имена на числата и стринговете, които ползваме. В предходния пример не само избегнахме повторението на код, но и документирахме факта, че числото 3.14159206 е всъщност добре известната в математиката константа ∏.

Константи трябва да дефинираме винаги, когато имаме нужда да ползваме числа или символни низове, за които не е очевидно от къде идват и какъв е логическият им смисъл. Константи е нормално да дефинираме и за всяко число или символен низ, който се ползва повече от веднъж в програмата.

Ето няколко типични ситуации, в които трябва да ползвате именувани константи:

-     За имена на файлове, с които програмата оперира. Те често трябва да се променят и затова е много удобно да са изнесени като кон­станти в началото на програмата.

-     За константи, участващи в математически формули и преобразу­вания. Доброто име на константата подобрява шансът при четене на кода да разберете смисъла на формулата.

-     За размери на буфери или блокове памет. Тези размери може да се наложи да се променят и е удобно да са изнесени като константи. Освен това използването на константата READ_BUFFER_SIZE вместо някакво магическо число 8192 прави кода много по-ясен и разби­раем.

Кога да не използваме константи?

Въпреки, че много книги препоръчват всички числа и символни низове, които не са 0, 1, -1, "" и null да бъдат изнасяни като константи, има някои изключения, в които изнасянето на константи е вредно. Запомнете, че изнасянето на константи се прави, за да се подобри четимостта на кода и поддръжката му във времето. Ако изнасянето на дадена константа не подобрява четимостта на кода, няма нужда да го правите.

Ето някои ситуации, в които изнасянето на текст или магическо число като константа не е полезно:

-     Съобщения за грешки и други съобщения към потребителя (примерно "въведете името си"): изнасянето им затруднява четенето на кода вместо да го улесни.

-     SQL заявки (ако използвате бази от данни, командите за извличане на информацията от базата данни се пише на езика SQL и пред­ставлява стринг). Изнасянето на SQL заявки като константи прави четенето на кода по-трудно и не се препоръчва.

-     Заглавия на бутони, диалози, менюта и други компоненти от потребителския интерфейс също не се препоръчва да се изнасят като константи, тъй като това прави кода по-труден за четене.

В .NET съществуват библиотеки, които подпомагат ин­тер­на­ци­о­на­ли­за­ци­я­та и позволяват да изнасяте съобщения за грешки, съобщения към потреби­теля и текстовете в потребителския интерфейс в специални ресурсни файлове, но това не са константи. Такъв подход се препоръчва, ако програмата, която пишете ще трябва да се интернационализира.

clip_image001[15]

Използвайте именувани константи, за да избегнете изпол­зването и повтарянето на магически числа и стрингове в кода и най-вече, за да подобрите неговата четимост. Ако въвеждането на именувана константа затруднява чети­мостта на програмата, по-добре оставете твърдо зададе­ната стойност в кода!

Правилно използване на конструкциите за управление

Конструкциите за управление са циклите и условните конструкции. Сега ще разгледаме добрите практики за правилното им използване.

Със или без къдрави скоби?

Циклите и условните конструкции позволяват тялото да не се обгражда със скоби и да се състои от един оператор (statement). Това е опасно. Вижте следния пример:

static void Main()

{

      int two = 2;

      if (two == 1)

            Console.WriteLine("This is the ...");

            Console.WriteLine("... number one.");

 

      Console.WriteLine(

            "This is an example of an if clause without curly brackets.");

}

Очакваме да се изпише само последното изречение? Резултатът е малко неочакващ:

clip_image013

Появява се един допълнителен ред. Това е защото в if-клаузата влиза само първия оператор (statement) след нея. Вторият е просто неправилно подравнен и объркващ.

clip_image001[16]

Винаги заграждайте тялото на циклите и условните конструкции с къдрави скоби – { и }.

Правилно използване на условни конструкции

Условни конструкции в C# са if-else операторите и switch-case операторите.

if (condition)

{

 

}

else

{

 

}

Дълбоко влагане на if-конструкции

Дълбокото влагане на if-конструкции е лоша практика, защото прави кода сложен и труден за четене. Ето един пример:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

private int Max(int a, int b, int c, int d)

{

      if (a < b)

      {

            if (b < c)

            {

                  if (c < d)

                  {

                        return d;

                  }

                  else

                  {

                        return c;

                  }

            }

            else if (b > d)

            {

                  return b;

            }

            else

            {

                  return d;

            }

      }

      else if (a < c)

      {

            if (c < d)

            {

                  return d;

            }

            else

            {

                  return c;

            }

      }

      else if (a > d)

      {

            return a;

      }

      else

      {

            return d;

      }

}

Този код е напълно нечетим. Причината е, че има прекалено дълбоко влагане на if конструкциите една в друга. За да се подобри четимостта на този код, може да се въведат един или няколко метода, в които да се изнесе част от сложната логика. Ето как може да се преработи кода, за да се намали вложеността на условните конструкции и да стане по-разби­раем:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

private int Max(int a, int b)

{

      if (a < b)

      {

            return b;

      }

      else

      {

            return a;

      }

}

 

private int Max(int a, int b, int c)

{

      if (a < b)

      {

            return Max(b, c);

      }

      else

      {

            return Max(a, c);

      }

}

 

private int Max(int a, int b, int c, int d)

{

      if (a < b)

      {

            return Max(b, c, d);

      }

      else

      {

            return Max(a, c, d);

      }

}

Изнасянето на част от кода в отделен метод и най-лесния и ефективен начин да се намали вложеността на група условни конструкции, като се запази логическият им смисъл.

Преработеният метод е разделен на няколко по-малки. Така резултатът като цяло е с 9 реда по-малко. Всеки от новите методи е много по-прост и лесен за четене. Като страничен ефект получаваме допълнително 2 метода, които можем да използваме и за други цели.

Правилно използване на цикли

Правилното използване на различните конструкции за цикли е от значе­ние при създаването на качествен софтуер. В следва­щи­те пара­графи ще се запознаем с някои принципи, които ни помагат да опре­де­лим кога и как да използваме определен вид цикъл.

Избиране на подходящ вид цикъл

Ако в дадена ситуация не можем да решим дали да използваме for, while или do-while цикъл, можем лесно да решим проблема, при­дър­жай­ки се към следващите принципи:

Ако се нуждаем от цикъл, който да се изпълни определен брой пъти, то е добре да използваме for цикъл. Този цикъл се използва в прости случаи, когато не се налага да прекъсваме изпълнението. При него още в нача­лото зада­ва­ме параметрите на цикъла и в общия случай, в тялото не се грижим за контрола му. Стойността на брояча вътре в тялото на цикъла не трябва да се променя.

Ако е необходимо да следим някакви условия, при които да прекратим изпълнението на цикъла, тогава вероятно е по-добре да използваме while цикъл. while цикълът е подходящ в случаи, когато не знаем колко точно пъти трябва да се изпълни тялото цикъла. При него изпълнението продължава, докато не се достигне дадено условие за край. Ако имаме налице пред­поставките за използване на while цикъл, но искаме да сме сигур­ни, че тялото ще се изпълни поне веднъж, то в такъв случай трябва да изпол­зва­ме do-while цикъл.

Не влагайте много цикли

Както и при условните конструкции, и при циклите е лоша практика да имаме дълбоко влагане. Дълбокото влагане обикновено се получава от голям брой цикли и условни конструкции, поставени една в друга. Това прави кода сложен и труден за четене и поддръжка. Такъв код лесно може да се подобри, като се отдели част от логиката в отделен метод. Съвре­менните среди за разработка могат да правят такава преработка на кода автоматично (ще обясним за това в секцията за преработка на кода).

Защитно програмиране

Защитно програмиране (defensive programming) е термин обозначаващ практика, която е насочена към защита на кода от некоректни данни. Защитното програмиране пази кода от грешки, които никой не очаква. То се имплементира чрез проверка на коректността на всички входни данни. Това са данните, идващи от външни източници, входните параметри на методите, конфигурационни файлове и настройки, данни въведени от пот­ребителя, дори и данни от друг локален метод.

Защитното програмиране изисква всички данни да се проверяват, дори да идват от източник, на когото се вярва. По този начин, ако в този източник има грешка (бъг), то тя ще бъде открита по-бързо.

Защитното програмиране се имплементира чрез assertions, изключения и други средства за управление на грешки.

Assertions

Това са специални условия, които винаги трябва да са изпълнени. Неизпълнението им завършва с грешка. Ето един бърз пример:

void LoadTemplates(string fileName)

{

      bool templatesFileExist = File.Exists(fileName);

      Debug.Assert(templatesFileExist,

            "Can't load templates file: " + fileName);

}

Assertions vs. Exceptions

Изключенията са анонси за грешка или неочаквано събитие. Те информират ползвателя на кода за грешка. Изключенията могат да бъдат "хванати" и изпълнението може да продължи.

Assertions (без наложил се термин на български) са най-общо фатални грешки. Не могат да бъдат хванати или обработени. Винаги индикират бъг в кода. Приложението не може да продължи.

Assertions могат да се изключват. По замисъл те трябва да са включени само по време на разработка, докато се открият всички бъгове. Когато бъдат изключени всички проверки в тях спират да се изпълняват. Идеята на изключването е, че след края на разработката, тези проверки не са повече нужни и само забавят софтуера.

Ако дадена проверка е смислено да продължи да съществува след края на разработката (примерно проверява входни данни на метод, които идват от потребителя), то тази проверка е неправилно имплементирана с assertions и трябва да бъде имплементирана с изключения.

clip_image001[17]

Assertions се използват само на места, на които трябва дадено условие да бъде изпълнено и единствената при­чина да не е, е да има бъг в програмата.

Защитно програмиране с изключения

Изключенията (exceptions) предоставят мощен механизъм за централизи­рано управление на грешки и непредвидени ситуации. В главата "Обра­ботка на изключения" те са описани подробно.

Изключенията позволяват проблемните ситуации да се обработват на много нива. Те улесняват писането и поддръжката на надежден програмен код.

Разликата между изключенията и assertions е в това, че изключенията в защитното програмиране се използват най-вече за защитаване на публич­ния интерфейс на един компонент. Този механизъм се нарича fail-safe (в свободен превод "проваляй се грациозно" или "подготвен за грешки").

Ако методът archive, описан малко по-нагоре, беше част от публичния интерфейс на архивиращ компонент, а не вътрешен метод, то този метод би трябвало да бъде имплементиран така:

public int Archive(PersonData user, bool persistent)

{

      if (user == null)

            throw new StorageException("null parameter");

 

      // Do some processing

      int resultFromProcessing = ...

 

      Debug.Assert(resultFromProcessing >= 0,

            "resultFromProcessing is negative. There is a bug");

 

      return resultFromProcessing;

}

Assert остава, тъй като той е предвиден за променлива създадена вътре в метода.

Изключенията трябва да се използват, за да се уведомят другите части на кода за проблеми, които не трябва да бъдат игнорирани. Хвърлянето на изключение е оправдано само в ситуации, които наистина са изключи­телни и трябва да се обработят по някакъв начин. За повече информация за това кои ситуации са изключителни и кои не погледнете главата "Обработка на изключения".

Ако даден проблем може да се обработи локално, то обработката трябва да се направи в самия метод и изключение не трябва да се хвърля. Ако даден проблем не може да се обработи локално, той трябва да бъде прехвърлен към извикващия метод.

Трябва да се хвърлят изключения с подходящо ниво на абстракция. Пример: GetEmployeeInfo() може да хвърля EmployeeException, но не и FileNotFoundException. Погледнете последният пример, той хвърля StorageException, а не NullReferenceException.

Повече за добрите практики при управление на изключенията можете да прочетете от секцията "Добри практики при работа с изключения" на главата "Обработка на изключения".

Документация на кода

C# спецификацията позволява писане на коментари в кода. Вече се запознахме с основните начини на писане на коментари. В следващите няколко параграфа ще обясним как се пишат ефективни коментари.

Самодокументиращ се код

Коментарите в кода не са основният източник на документация. Запом­нете това! Добрият стил на програмиране е най-добрата документация! Самодокументиращ се код е такъв, на който лесно се разбира основната му цел, без да е необходимо да има коментари.

clip_image001[18]

Най-добрата документация на кода е да пишем качествен код. Лошият код не трябва да се коментира, а трябва да се пренапише, така че сам да описва себе си. Коментарите в програмата само допълват документацията на добре напи­сания код.

Характеристики на самодокументиращия се код

Характеристики на самодокументиращия се код са добра структура на програмата – подравняване, организация на кода, използване на ясни и лесни за разбиране конструкции, избягване на сложни изрази. Такива са още употребата на подходящи имена на променливи, методи и класове и употребата на именувани константи, вместо "магически" константи и текстови полета. Реализацията трябва да е опростена максимално, така че всеки да я разбере.

Самодокументиращ се код – важни въпроси

Въпроси, които трябва да си зададем преди да отговорим на въпроса дали кодът е самодокументиращ се:

-     Подходящо ли е името на класа и показва ли основната му цел?

-     Става ли ясно от интерфейса как трябва да се използва класа?

-     Показва ли името на метода основната му цел?

-     Всеки метод реализира ли една добре определена задача?

-     Имената на променливите съответстват ли на тяхната употреба?

-     Групирани ли са свързаните един с друг оператори?

-     Само една задача ли изпълняват конструкциите за итерация (циклите)?

-     Има ли дълбоко влагане на условни конструкции?

-     Показва ли организацията на кода неговата логическата структура?

-     Дизайнът недвусмислен и ясен ли е?

-     Скрити ли са детайлите на имплементацията възможно най-много?

"Ефективни" коментари

Коментарите понякога могат да навредят повече, отколкото да помогнат. Добрите коментари не повтарят кода и не го обясняват – те изясняват неговата идея. Коментарите трябва да обясняват на по-високо ниво какво се опитваме да постигнем. Писането на коментари помага да осмислим по-добре това, което искаме да реализираме.

Ето един пример за лоши коментари, които повтарят кода и вместо да го направят по-лесно четим, го правят по-тежък за възприемане:

public List<int> FindPrimes(int start, int end)

{

      // Create new list of integers

      List<int> primesList = new List<int>();

      // Perform a loop from start to end

      for (int num = start; num <= end; num++)

      {

            // Declare boolean variable, initially true

            bool prime = true;

            // Perform loop from 2 to sqrt(num)

            for (int div = 2; div <= Math.Sqrt(num); div++)

            {

                  // Check if div divides num with no remainder

                  if (num % div == 0)

                  {

                        // We found a divider -> the number is not prime

                        prime = false;

                        // Exit from the loop

                        break;

                  }

                  // Continue with the next loop value

            }

 

            // Check if the number is prime

            if (prime)

            {

                  // Add the number to the list of primes

                  primesList.Add(num);

            }

      }

 

      // Return the list of primes

      return primesList;

}

Ако вместо да слагаме наивни коментари, ги ползваме, за да изясним неочевидните неща в кода, те могат да са много полезни. Вижте как бихме могли да коментираме същия код, така че да му подобрим четимостта:

/// <summary>

/// Finds primes from a range [a,b] and returns them in a list.

/// </summary>

/// <param name="start">Top of range</param>

/// <param name="end">End of range</param>

/// <returns>

/// a list of all the found primes

/// </returns>

public List<int> FindPrimes(int start, int end)

{

      List<int> primesList = new List<int>();

      for (int num = start; num <= end; num++)

      {

            bool isPrime = IsPrime(num);

            if (isPrime)

            {

                  primesList.Add(num);

            }

      }

      return primesList;

}

 

/// <summary>

/// Checks if a number is prime by checking for any

/// dividers in the range [2, sqrt(number)].

/// </summary>

/// <param name="number">The number to be checked</param>

/// <returns>True if prime</returns>

public bool IsPrime(int number)

{

      for (int div = 2; div <= Math.Sqrt(number); div++)

      {

            if (number % div == 0)

            {

                  return false;

            }

      }

 

      return true;

}

Логиката на кода е очевидна и няма нужда от коментари. Достатъчно е да се опише за какво служи даденият метод и основната му идея (как работи) в едно изре­чение.

При писането на "ефективни" коментари е добра практика да се използва псевдокод, когато е възможно. Коментарите трябва да се пишат, когато се създава самия код, а не след това.

Продуктивността никога не е добра причина, за да не се пишат комен­тари. Трябва да се документира всичко, което не става ясно от кода. Поставянето на излишно много коментари е толкова вредно колкото и липсата на такива.

Лошият код не става по-добър с повече коментари. За да стане добър код, просто трябва да се преработи.

Преработка на кода (Refactoring)

Терминът Refactoring се появява през 1993 и е популяризиран от Мартин Фаулър в едноименната му книга по темата. В тази книга се разглеждат много техники за преработка на код. Нека и ние разгледаме няколко.

Дадена програма се нуждае от преработка, при повторение на код. Повто­рението на код е опасно, защото когато трябва да се променя, трябва да се про­меня на няколко места и естествено някое от тях може да бъде пропуснато и така да се получи несъответствие. Избягването на повтарящ се код може да стане чрез изваждане на метод или преместване на код от клас-наследник в базов клас.

Преработка се налага и при методи, които са нараснали с времето. Прекалената дължината на метод е добра причина да се замислим дали методът не може да се раздели логически на няколко по-малки и по-прости метода.

При цикъл с прекалено дълбоко ниво на влагане трябва да се замислим дали не можем да извадим в отделен метод част от кода му. Обикновено това подобрява четимостта на кода и го прави по-лесен за разбиране.

Преработката е наложителна при клас, който изпълнява несвързани отго­ворности (weak cohesion). Клас, който не предоставя достатъчно добро ниво на абстракция също трябва да се преработи.

Дългият списък с параметри и публичните полета също трябва да са в графата "да се поправи". Тази графа трябва да допълни и когато една промяна налага да се променят паралелно още няколко класа. Прекалено свързани класове или недостатъчно свързани класове също трябва да се преработят.

Преработка на код на ниво данни

Добра практика е в кода да няма "магически" числа. Те трябва да бъдат заменени с константи. Променливите с неясни имена трябва да се преиме­нуват. Дългите условни изрази могат да бъдат преработени в отделни методи. За резултата от сложни изрази могат да се използват междинни променливи. Група данни, които се появяват заедно могат да се прера­ботят в отделен клас. Свързаните константи е добре да се преместят в изброими типове (enumerations).

Добра практика е всички задачи от един по-голям метод, които не са свързани с основната му цел, да се "преместят" в отделни методи (extract method). Сходни задачи трябва да се групират в общи класове, сходните класове – в общ пакет. Ако група класове имат обща функционалност, то тя може да се изнесе в базов клас.

Не трябва да има циклични зависимости между класовете – те трябва да се премахват. Най-често по-общият клас има референция към по-специа­лизирания (връзка родител-деца).

Ресурси

clip_image015

Библията за качествен програмен код се казва "Code Complete" и през 2004 година излезе във второ издание. Авторът й Стийв Макконъл е световноизвестен експерт по писане на качествен софтуер. В книгата можете да откриете много повече примери и детайлни описания на раз­лични проблеми, които не успяхме да разгледаме.

clip_image017

Друга добра книга е "Refactoring" на Мартин Фаулър. Тази книга се смята за библията в преработката на код. В нея за първи път са описани понятията "extract method" и други, стоящи в основата на съвременните шаблони за преработка на съществуващ код.

 

Упражнения

1.     Вземете кода от първия пример в тази глава и го направете качествен.

2.     Прегледайте собствения си код досега и вижте какви грешки допускате. Обърнете особено внимание на тях и помислете защо ги допускате. Постарайте се в бъдеще да не правите същите грешки.

3.     Отворете чужд код и се опитайте само на базата на кода и доку­ментацията да разберете какво прави той. Има ли неща, които не ви стават ясни от първия път? А от втория? Какво бихте променили в този код? Как бихте го написали вие?

4.     Разгледайте класове от CTS. Намирате ли примери за некачествен код?

5.     Използвали ли сте (виждали ли сте) някакви код конвенции. През приз­мата на тази глава смятате ли, че са добри или лоши?

6.     Дадена е квадратна матрица с големина n x n клетки. Въртящо обхождане на матрица наричаме такова обхождане, което започва от най-горната най-лява клетка на матрицата и тръгва към най-долната дясна. Когато обхождането не може да продължи в текущата посока (това може да се случи, ако е стигнат краят на матрицата или е достигната вече обходена клетка) посоката се сменя на следващата възможна по часовниковата стрелка. Осемте възможни посоки са:clip_image019

      Когато няма свободна празна клетка във всички възможни посоки, обхождането продължава от първата свободна клетка с възможно най-малък ред и възможно най-близко до началото на този ред. Обхождането приключва, когато няма свободна празна клетка в цялата матрица. Задачата е да се напише програма, която чете от конзолата цяло число n (1 n ≤ 100) и изписва запълнената матрица също на конзолата.

    Примерен вход:

n = 6

   Примерен изход:

 1  16  17  18  19  20

15   2  27  28  29  21

14  31   3  26  30  22

13  36  32   4  25  23

12  35  34  33   5  24

11  10   9   8   7   6

      Вашата задача е да свалите от този адрес решение на горната задача:
http://introcsharpbook.googlecode.com/svn/trunk/book/resources/High-Quality-Code.rar и да го преработите според концепциите за качествен код. Може да ви се наложи да оправяте и бъгове в решението.

Решения и упътвания

1.     Използвайте [Ctrl+K, Ctrl+F] във Visual Studio или C# Developer и вижте разликите. След това отново с помощта на средата преименувайте променливите, премахнете излишните оператори и променливи и направете текста, който се отпечатва на екрана по-смислен.

2.     Внимателно следвайте препоръките за конструиране на качествен прог­рамен код от настоящата тема. Записвайте грешките, които правите най-често и се постарайте да ги избягвате.

3.     Вземете като пример някой качествено написан софтуер. Вероятно ще откриете неща, които бихте написали по друг начин или тази глава съветва да се напишат по друг начин. Отклоненията са възможни и са съвсем нормални. Разликата между качествения и некачествения софтуер е в последователността на спазване на правилата.

4.     Кодът от CTS  е писан от инженери с дългогодишен опит и в него рядко ще срещнете некачествен код. Въпреки всичко се срещат недоразуме­ния като използване на сложни изрази, непра­вилно именувани промен­ливи и други.

5.     Разгледайте код, кой вие или ваши колеги са писали.

6.     Прегледайте всички изучени концепции и ги приложете върху дадения код. Първо оправете кода, осмислете как работи и чак тогава оправете бъговете, които откриете при неговата работа.

Демонстрации (сорс код)

Изтеглете демонстрационните примери към настоящата глава от книгата: Качествен-програмен-код-Демонстрации.zip.

Дискусионен форум

Коментирайте книгата и задачите в нея във: форума на софтуерната академия.


Share

Коментирай