Database
 sql >> база данни >  >> RDS >> Database

Ключови индикатори за проектиране на проблеми

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

Този дизайн обикновено има следните индикатори (един или няколко наведнъж):

  • Трудност (трудно е да се промени кодът, тъй като простата промяна засяга много места);
  • Неподвижност (сложно е да се раздели кодът на модули, които могат да се използват в други програми);
  • Вискозитет (доста е трудно да се разработи или тества кодът);
  • Ненужна сложност (в кода има неизползвана функционалност);
  • Ненужно повторение (копиране/поставяне);
  • Лоша четливост (трудно е да се разбере за какво е предназначен кодът и да се поддържа);
  • Крехкост (лесно е да се наруши функционалността дори при малки промени).

Трябва да можете да разберете и разграничите тези характеристики, за да избегнете проблемен дизайн или да предвидите възможни последици от използването му. Тези показатели са описани в книгата „Agile Principles, Patterns, And Practices in C#“ от Робърт Мартин. Въпреки това, в тази статия, както и в други статии за преглед, има кратко описание и няма примери за код.

Ще премахнем този недостатък, като се спираме на всяка функция.

Твърдост

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

Един от популярните случаи на ригидност е изрично да се посочат типовете класове, вместо да се използват абстракции (интерфейси, базови класове и т.н.). По-долу можете да намерите пример за кода:

<пред>клас A{ B _b; public A() { _b =new B(); } public void Foo() { // Направете някаква персонализирана логика. _b.Направи нещо(); // Направете някаква персонализирана логика. }}клас B{ public void DoSomething() { // Направете нещо }}

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

Заобикалянето е абстракция, която трябва да въведе интерфейса IComponent чрез конструктора на клас A. В този случай той вече няма да зависи от конкретния клас В и ще зависи само от интерфейса IComponent. Клас В от своя страна трябва да имплементира интерфейса IComponent.

interface IComponent{ void DoSomething();}class A{ IComponent _component; public A(IComponent component) { _component =component; } void Foo() { // Направете някаква персонализирана логика. _component.DoSomething(); // Направете някаква персонализирана логика. }}клас B :IComponent{ void DoSomething() { // Направи нещо }}

Нека предоставим конкретен пример. Да приемем, че има набор от класове, които регистрират информацията – ProductManager и Consumer. Тяхната задача е да съхраняват даден продукт в базата данни и да го поръчват съответно. И двата класа регистрират съответните събития. Представете си, че в началото имаше лог във файл. За да направите това, беше използван класът FileLogger. Освен това класовете бяха разположени в различни модули (асамблеи).

// Модул 1 (Клиент)static void Main(){ var product =new Product("milk"); var productManager =нов ProductManager(); productManager.AddProduct(продукт); var консуматор =нов потребител(); потребител.ПокупкаПродукт(продукт.Име);}// Модул 2 (Бизнес логика)публичен клас ProductManager{ частен само за четене FileLogger _logger =new FileLogger(); public void AddProduct(продукт продукт) { // Добавяне на продукта към базата данни. _logger.Log("Продуктът е добавен."); }}public class Consumer{ private readonly FileLogger _logger =new FileLogger(); public void PurchaseProduct(string product) { // Купете продукта. _logger.Log("Продуктът е закупен."); }}public class Product{ public string Name { get; частен комплект; } публичен продукт(име на низ) { Име =име; }}// Модул 3 (Реализация на Logger) публичен клас FileLogger{ const string FileName ="log.txt"; public void Log(string message) { // Записване на съобщението във файла. }}

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

// Модул 1 (Клиент)static void Main(){ var logger =new FileLogger(); var продукт =нов продукт("мляко"); var productManager =нов ProductManager(logger); productManager.AddProduct(продукт); var консуматор =нов потребител(регистратор); consumer.PurchaseProduct(product.Name);}// Модул 2 (Бизнес логика)class ProductManager{ private readonly ILogger _logger; public ProductManager(ILogger logger) { _logger =logger; } public void AddProduct(продукт продукт) { // Добавете продукта към базата данни. _logger.Log("Продуктът е добавен."); }}public class Consumer{ private readonly ILogger _logger; public Consumer(ILogger logger) { _logger =logger; } public void PurchaseProduct(string product) { // Купете продукта. _logger.Log("Продуктът е закупен."); }}public class Product{ public string Name { get; частен комплект; } публичен продукт(име на низ) { Име =име; }}// Модул 3 (интерфейси) публичен интерфейс ILogger{ void Log(string message);}// Модул 4 (Реализация на Logger) публичен клас FileLogger :ILogger{ const string FileName ="log.txt"; public virtual void Log(string message) { // Запишете съобщението във файла. }}

В този случай при промяна на типа регистратор е достатъчно да промените клиентския код (Main), който инициализира регистратора и го добавя към конструктора на ProductManager и Consumer. По този начин затворихме класовете бизнес логика от модификацията на типа регистратор според изискванията.

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

static void Main(){ var rectangle =new Rectangle() { W =3, H =5 }; var circle =new Circle() { R =7 }; var shapes =new Shape[] { правоъгълник, кръг}; ShapeHelper.ReportShapesSize(shapes);}class ShapeHelper{ private static double GetShapeArea(Shape shape) { if (формата е Rectangle) { return ((Rectangle)shape).W * ((Rectangle)shape).H; } if (формата е Circle) { return 2 * Math.PI * ((Circle)shape).R * ((Circle)shape).R; } throw new InvalidOperationException("Неподдържана форма"); } public static void ReportShapesSize(Shape[] shapes) { foreach(Shape shape in shapes) { if (формата е правоъгълник) { double area =GetShapeArea(shape); Console.WriteLine($"Площта на правоъгълника е {area}"); } if (формата е Circle) { double area =GetShapeArea(shape); Console.WriteLine($"Областта на кръга е {area}"); } } }}public class Shape{ }public class Rectangle :Shape{ public double W { get; комплект; } public double H { get; комплект; }}public class Circle :Shape{ public double R { get; комплект; }}

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

static void Main(){ var rectangle =new Rectangle() { W =3, H =5 }; var circle =new Circle() { R =7 }; var shapes =new Shape[]() { правоъгълник, кръг}; ShapeHelper.ReportShapesSize(shapes);}class ShapeHelper{ public static void ReportShapesSize(Shape[] shapes) { foreach(Shape shape in shapes) { shape.Report(); } }}public abstract class Shape{ public abstract void Report();}public class Rectangle :Shape{ public double W { get; комплект; } public double H { get; комплект; } публично замяна void Report() { двойна площ =W * H; Console.WriteLine($"Площта на правоъгълника е {area}"); }}public class Circle :Shape{ public double R { get; комплект; } public override void Report() { double area =2 * Math.PI * R * R; Console.WriteLine($"Областта на кръга е {area}"); }}

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

Неподвижност

Можем да наблюдаваме неподвижността, когато разделяме кода на модули за многократна употреба. В резултат на това проектът може да спре да се развива и да бъде конкурентоспособен.

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

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

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

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

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

Вискозитет

Има два вида:

  • Вискозитет на проявяване
  • Вискозитет на околната среда

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

Като прост пример можем да разгледаме работата с константи, които трябва да бъдат поставени (По проект) в отделен модул (Модул 1), който да се използва от други компоненти (Модул 2 и Модул 3).

// Модул 1 (Константи)статичен клас Константи{ public const decimal MaxSalary =100M; public const int MaxNumberOfProducts =100;} // Финансов модул#using Module1static class FinanceHelper{ public static bool ApproveSalary(десетична заплата) { return salary <=Constants.MaxSalary; }} // Маркетингов модул#using Module1class ProductManager{ public void MakeOrder() { int productsNumber =0; while(productsNumber++ <=Constants.MaxNumberOfProducts) { // Купете някакъв продукт } }}

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

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

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

Ненужна сложност

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

class DataManager{ object[] GetData() { // Извличане и връщане на данни }}

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

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

interface IProcessor{ void Process(string message);}

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

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

Защо да си губите времето за писане на потенциално неизползван код? Понякога QA трябва да тества този код, тъй като той всъщност е публикуван и е отворен за използване от клиенти на трети страни. Това също отлага времето за пускане. Включването на функция за бъдещето си струва само ако възможната полза от нея надвишава разходите за нейното разработване и тестване.

Ненужно повторение

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

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

Лоша четливост

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

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

void Process_true_false(string trueorfalsevalue){ if (trueorfalsevalue.ToString().Length ==4) { // Това означава, че trueorfalsevalue вероятно е "true". Направете нещо тук. } else if (trueorfalsevalue.ToString().Length ==5) { // Това означава, че trueorfalsevalue вероятно е "false". Направете нещо тук. } else { throw new Exception("не е вярно за false. това не е хубаво. връщане.") }}

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

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

Трето, текстът на изключението не съответства на официалния стил. Четейки такива текстове, може да има усещане, че кодът е създаден от любител (все пак може да има някакъв проблем). Методът може да бъде пренаписан, както следва, ако приема булева стойност:

public void Process(bool value){ if (value) { // Направете нещо. } else { // Направете нещо. }}

Ето още един пример за рефакторинг, ако все пак трябва да вземете низ:

public void Process(string value){ bool bValue =false; if (!bool.TryParse(value, out bValue)) { throw new ArgumentException($"{value} не е булева"); } if (bValue) { // Направете нещо. } else { // Направете нещо. }}

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

Крехкост

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

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

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

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

static void Main(){ if (Helper.Authorize(1, "/pictures")) { Console.WriteLine("Authorized"); }}class Helper{ public static bool Authorize(int roleId, string resourceUri) { if (roleId ==1 || roleId ==10) { if (resourceUri =="/pictures") { return true; } } if (roleId ==1 || roleId ==2 &&resourceUri =="/admin") { return true; } върне false; }}

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

static void Main(){ var picturesResource =new Resource() { Uri ="/pictures" }; PictureResource.AddRole(1); if (picturesResource.IsAvailable(1)) { Console.WriteLine("Authorized"); }}class Resource{ private List _roles =new List(); публичен низ Uri { get; комплект; } public void AddRole(int roleId) { _roles.Add(roleId); } public void RemoveRole(int roleId) { _roles.Remove(roleId); } public bool IsAvailable(int roleId) { return _roles.Contains(roleId); }}

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

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

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

Заключение

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


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Търсене в таблици в SortCL-съвместими IRI работни места

  2. Как да маскирате таблици и да запазите референтната цялост

  3. Релационният модел

  4. Въведение в API за едновременно събиране в Java

  5. Част 2 – Как да организираме диаграма на голяма база данни