Зниження складності класу


10

Я переглянув кілька відповідей і шукав у Google, але не зміг знайти нічого корисного (тобто, що не мало б ніяких побічних ефектів).

Моя проблема, абстрактно, полягає в тому, що у мене є об'єкт і потрібно виконувати довгу послідовність операцій над ним; Я думаю про це як про якусь складальну лінію, як про побудову машини.

Я вважаю, що ці об'єкти будуть називатися методами об'єктів .

Тож у цьому прикладі в якийсь момент я мав би CarWithoutUpholstery, на якій мені тоді потрібно запустити installBackSeat, installFrontSeat, installWoodenInserts (операції не заважають одна одній, і навіть можуть бути виконані паралельно). Ці операції виконуються CarWithoutUpholstery.worker () і дають новий об'єкт, який би був CarWithUpholstery, на якому я б тоді запустив, можливо, cleanInsides (), verifyNoUpholsteryDefects () тощо.

Операції в одну фазу вже є незалежними, тобто я вже бореться з їх підмножиною, яка може бути виконана в будь-якому порядку (переднє і заднє сидіння можуть бути встановлені в будь-якому порядку).

Наразі моя логіка використовує Reflection для простоти реалізації.

Тобто, коли у мене є CarWithoutUpholstery, об'єкт перевіряє себе на предмет методів, які називаються performSomething (). У цей момент він виконує всі ці методи:

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

перевіряючи помилки та інше. Незважаючи на те, що порядок роботи є неважливим, я призначив лексикографічний наказ у випадку, якщо я коли-небудь виявляю якийсь порядок, важливий зрештою. Це суперечить YAGNI , але це коштувало дуже мало - простий сорт () - і це могло б врятувати масовий метод перейменування (або запровадити якийсь інший метод виконання тестів, наприклад, масив методів) вниз.

Інший приклад

Скажімо, що замість того, щоб будувати автомобіль, я повинен скласти звіт про секретну поліцію про когось і подати його моєму Evil Overlord . Моїм кінцевим об’єктом буде ReadyReport. Для його побудови я починаю з збору основної інформації (ім'я, прізвище, дружина ...). Це моя фаза А. Залежно від того, є подружжя чи ні, мені, можливо, доведеться перейти до фаз B1 або B2 і зібрати дані про сексуальність на одного або двох людей. Це складається з декількох різних запитів до різних Evil Minions, які контролюють нічне життя, вуличні камери, квитанції про продаж секс-магазинів і що ні. І так далі, і так далі.

Якщо у потерпілого немає родини, я навіть не вступлю до фази отримання інформації про сім'ю, але якщо це зробити, то неважливо, чи спочатку я націлююсь на батька чи матір чи братів і сестер (якщо такі є). Але я не можу цього зробити, якщо я не виконав FamilyStatusCheck, який, отже, належить до більш ранньої фази.

Це все чудово працює ...

  • якщо мені потрібна додаткова операція, мені потрібно лише додати приватний метод,
  • якщо операція є загальною для декількох фаз, я можу її успадкувати від суперкласу,
  • операції прості та самостійні. Ні одне значення з однієї операції не завжди вимагає будь - якого з інших (операцій , які дійсно виконуються в іншій фазі),
  • об'єктам вниз лінії не потрібно виконувати багато тестів , так як вони не могли навіть існувати , якщо їх об'єкти творці не перевірили ті умови , в першу чергу. Тобто, розміщуючи вставки на приладовій панелі, прибираючи приладну панель і перевіряючи приладову панель, мені не потрібно перевіряти, що приладова панель насправді є .
  • це дозволяє легко протестувати. Я можу легко знущатися над частковим об’єктом і запускати на ньому будь-який метод, а всі операції є детермінованими чорними полями.

... але ...

Проблема виникла, коли я додав одну останню операцію в один із моїх об'єктів методу, через що загальний модуль перевищив обов'язковий індекс складності ("менше N приватних методів").

Я вже взяв цю справу наверх і припустив, що в цьому випадку багатство приватних методів не є повістю катастрофи. Складність є , але вона є тому, що операція є складною, а насправді це не все так складно - вона просто довга .

Використовуючи приклад Evil Overlord, моя проблема полягає в тому, що Злий Оверлорд ( він же, хто не повинен відмовлятись ), запитуючи всю дієтичну інформацію, мої дієтичні мініони говорять мені, що мені потрібно запитувати ресторани, кухонні куточки, вуличні торговці, неліцензовані торгові вулиці, парник власники тощо, і Злий (суб) Оверлорд - відомий під назвою Той, Хто також не повинен бути відхилений - скаржиться, що я виконую надто багато запитів у фазі GetDietaryInformation.

Примітка . Я усвідомлюю, що з кількох точок зору це зовсім не проблема (ігнорування можливих проблем з роботою тощо). Все, що відбувається, полягає в тому, що конкретний показник нещасний, і для цього є виправдання.

Що я думаю, я міг би зробити

Окрім першого, усі ці варіанти є придатними і, я думаю, захисними.

  • Я переконався, що я можу бути підлим і заявляю половину своїх методів protected. Але я б використовував слабкість у процедурі тестування, і окрім того, що виправдовувати себе, коли потрапляють, мені це не подобається. Також це міра зупинки. Що робити, якщо кількість необхідних операцій подвоїться? Навряд чи, але що тоді?
  • Я можу розділити цю фазу довільно на AnnealedObjectAlpha, AnnealedObjectBravo та AnnealedObjectCharlie і мати третину операцій, що виконуються на кожному етапі. Мені здається, що це фактично додає складності (ще N-1 класи), без користі, крім проходження тесту. Звичайно, я можу стверджувати, що CarWithFrontSeatsInstalled та CarWithAllSeatsInstalled є логічно послідовними етапами. Ризик того, що Альфа пізніше вимагатиме методу Браво, невеликий і навіть менший, якщо я добре його зіграю. Але все ж.
  • Я можу купувати різні операції, віддалено схожі, в одній. performAllSeatsInstallation(). Це лише міра зупинки, яка збільшує складність однієї операції. Якщо мені коли-небудь потрібно буде робити операції A і B в іншому порядку, і я запакував їх всередині E = (A + C) і F (B + D), мені доведеться роз'єднати E і F і перетасувати код навколо .
  • Я можу використовувати масив лямбда-функцій і акуратно пройти повний чек, але я вважаю це незграбним. Однак це найкраща альтернатива поки що. Це позбулося б роздумів. Дві проблеми, які у мене є, полягають у тому, що мене, мабуть, попросять переписати всі об'єкти методу, не лише гіпотетичні CarWithEngineInstalled, і хоча це було б дуже гарною безпекою роботи, вона насправді не дуже сподобається; і що перевірка покриття коду має проблеми з лямбдами (які вирішуються, але все-таки ).

Тому...

  • Який, ти думаєш, мій найкращий варіант?
  • Чи є кращий спосіб, який я не розглядав? ( можливо, я б краще прийшов чистий і прямо запитав, що це? )
  • Хіба що цей дизайн безнадійно хибний, і я краще визнаю поразку та рів - ця архітектура взагалі? Не годиться для моєї кар’єри, але чи було б написання погано розробленого коду в майбутньому краще?
  • Чи є моїм поточним вибором насправді єдиний вірний шлях, і мені потрібно боротись, щоб встановити якісні показники (та / або інструментарій)? Для цього останнього варіанту мені знадобляться посилання ... Я не можу просто помахати рукою на @PHB під час бурчання. Це не ті показники, які ви шукаєте . Скільки б я не хотів би вміти

3
Або ви можете ігнорувати попередження "перевищена максимальна складність".
Роберт Харві

Якби я міг, я б. Якось ця річ з метрикою набула власної сакральної якості. Я продовжую намагатися переконати ПТБ прийняти їх як корисний інструмент, але лише інструмент. Можливо, мені все-таки вдасться ...
LSerni

Чи дійсно операції будують об'єкт, чи ви просто використовуєте метафору конвеєрного рядка, оскільки він поділяє конкретну властивість, яку ви згадуєте (багато операцій із частковим порядком залежності)? Важливе значення полягає в тому, що перший пропонує схему будівельника, а другий може запропонувати інший шаблон із більш детальними відомостями.
outis

2
Тиранія метрик. Метрики є корисними показниками, але їх застосування, не враховуючи, чому цей показник використовується, не є корисним.
Джейді

2
Поясніть людям нагорі різницю між істотною складністю та випадковою складністю. en.wikipedia.org/wiki/No_Silver_Bullet Якщо ви впевнені , що складність , що порушує правило має важливе значення, то якщо ви рефакторинг за programmers.stackexchange.com/a/297414/51948 ви , можливо , катання на ковзанах навколо правила і поширення зі складності. Якщо інкапсуляція речей у фази не є довільною (фази стосуються проблеми), а перероблення зменшує когнітивне навантаження для розробників, які підтримують код, то це має сенс зробити.
Фурманатор

Відповіді:


15

Тривала послідовність операцій здається, що це повинен бути власний клас, якому тоді необхідні додаткові об'єкти для виконання своїх обов'язків. Здається, ви близькі до цього рішення. Ця довга процедура може бути розбита на кілька етапів або етапів. Кожен крок або фаза може бути власним класом, який відкриває публічний метод. Ви хочете, щоб кожна фаза реалізувала один і той же інтерфейс. Тоді ваша складальна лінія відстежує список фаз.

Щоб побудувати на прикладі складання автомобіля, уявіть собі Carклас:

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

Я збираюся опустити реалізацію деяких компонентів, як IChassis, Seatі Dashboardзаради стислості.

Компанія Carне несе відповідальності за будівництво. Збірна лінія є, тому давайте інкапсулюємо це в класі:

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

Важливою відмінністю тут є те, що CarAssembler має перелік об'єктів фази складання, які реалізуються IAssemblyPhase. Тепер ви чітко маєте справу з публічними методами, тобто кожна фаза може бути перевірена сама собою, а ваш інструмент якості коду щасливіший, оскільки ви не засипаєте так багато в один клас. Процес складання теж мертва. Просто переведіть фази на фази і зателефонуйте, що Assembleпроїжджає в машині. Зроблено. IAssemblyPhaseІнтерфейс також мертвий просто з допомогою всього одного загальнодоступного методу , який приймає об'єкт автомобіля:

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

Тепер нам потрібні конкретні класи, що реалізують цей інтерфейс для кожної конкретної фази:

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

Кожна фаза окремо може бути досить простою. Деякі - це лише один лайнер. Деякі можуть просто засліпити додавання чотирьох сидінь, а інші повинні налаштувати приладну панель перед тим, як додати її до машини. Складність цього довго розробленого процесу поділяється на кілька класів для багаторазового використання, які легко перевірити.

Хоча ви сказали, що замовлення зараз не має значення, це може бути в майбутньому.

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

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

Ви можете підклас CarAssembler для складання конкретного виду автомобіля чи вантажівки. Кожна фаза - це одна одиниця всередині більшого цілого, що полегшує повторне використання коду.


Хіба що цей дизайн безнадійно хибний, і я краще визнаю поразку та рів - ця архітектура взагалі? Не добре для моєї кар’єри, але чи не дуже добре в довгостроковій перспективі буде написаний неправильно розроблений код?

Якщо вирішення того, що я написав, потрібно переписати, було чорною позначкою на моїй кар’єрі, то я б зараз працював копачем канави, і ви не повинні приймати жодних моїх порад. :)

Розробка програмного забезпечення - це вічний процес навчання, застосування, а потім перенавчання. Тільки тому, що ви вирішили переписати свій код, не означає, що вас звільнять. Якби це було так, не було б інженерів програмного забезпечення.

Чи є моїм поточним вибором насправді єдиний вірний шлях, і мені потрібно боротись, щоб встановити якісні показники (та / або інструментарій)?

Те, що ви окреслили, - це не Єдиний вірний шлях, проте майте на увазі, що показники коду також не є Єдиним істинним шляхом. Показники коду слід розглядати як попередження. Вони можуть вказувати на проблеми з технічним обслуговуванням у майбутньому. Вони є настановами, а не законами. Коли ваш інструмент метрики коду щось вказує, я спершу досліджую код, щоб перевірити, чи правильно він реалізує принципи SOLID . Багато разів переробляючи код, щоб зробити його більш твердим, задовольнить інструменти метрики коду. Іноді це не так. Ви повинні приймати ці показники в кожному конкретному випадку.


1
Так, набагато краще, ніж мати один клас богів.
BЈович

Цей підхід працює, але в основному тому, що ви можете отримати елементи для автомобіля без параметрів. Що робити, якщо вам потрібно було їх передати? Тоді ви можете викинути все це у вікно і повністю переосмислити процедуру, адже ви насправді не маєте фабрику автомобілів із 150 параметрами, це гайки.
Енді

@DavidPacker: Навіть якщо вам потрібно параметризувати купу матеріалів, ви можете інкапсулювати це також у якийсь клас Config, який ви передаєте в конструктор об'єкта "асемблер". У цьому прикладі автомобіля можна налаштувати колір фарби, колір салону та матеріали, комплект обробки, двигун та багато іншого, але не обов’язково буде 150 налаштувань. Ймовірно, у вас буде десяток або близько того, що піддається об'єкту опцій.
Грег Бургхардт

Але лише додавання конфігурації буде протистояти цілі красивого циклу, що є величезною прикрою, оскільки вам доведеться проаналізувати конфігурацію та призначити її належним методам, які взамін дадуть вам належні об’єкти. Тож ви, мабуть, закінчилися чимось подібним до того, який був оригінальний дизайн.
Енді

Розумію. Замість того, щоб мати один клас і п’ятдесят методів, я б фактично мав п’ятдесят художніх класів асемблера та один клас оркестрування. Врешті-решт результат здається таким самим, але і продуктивним, але компонування коду чистіша. Мені це подобається. Буде трохи незручніше додавати операції (мені доведеться їх налаштувати в їх клас, плюс повідомити клас оркестрування; я не бачу простого виходу з цієї необхідності), але все одно мені це дуже подобається. Дозвольте мені пару днів вивчити цей підхід.
LSerni

1

Я не знаю, чи це можливо для вас, але я б думав про використання більш орієнтованого на дані підходу. Ідея полягає в тому, щоб ви зафіксували деяке (декларативне) вираження правил і обмежень, операцій, залежностей, стану тощо ... Тоді ваші класи та код більш загальні, орієнтовані на дотримання правил, ініціалізацію поточного стану примірників, виконання операцій з змінити стан, використовуючи декларативний захоплення правил і зміни стану, а не використовуючи код більш безпосередньо. Можна використовувати декларації даних мовою програмування або, деяким інструментом DSL, або механізмом правил або логіки.


Деякі операції реально реалізуються таким чином (як списки правил). Для прикладу автомобіля під час встановлення коробки з запобіжниками, ліхтарями та штепсельними розетками я фактично не використовую три різні методи, а лише один, який загалом поміщає двоконтактні електричні речімаджики на місце, і займає Список трьох списків запобіжників , ліхтарі та вилки. Більшість інших методів, на жаль, не піддаються такому підходу.
LSerni

1

Якось проблема нагадує мені залежності. Ви хочете автомобіль у певній конфігурації. Як і Грег Бургхардт, я б ходив з предметами / класами для кожного кроку / елемента /….

Кожен крок оголошує, що він додає до суміші (що це таке provides) (може бути декілька речей), а також те, що потрібно.

Потім ви десь визначаєте остаточну необхідну конфігурацію і маєте алгоритм, який розглядає, що вам потрібно, і вирішує, які кроки потрібно виконати / які частини потрібно додати / які документи потрібно зібрати.

Алгоритми вирішення залежності не так вже й важкі. Найпростіший випадок, коли кожен крок забезпечує одне, і дві речі не забезпечують одне і те ж, а залежності прості (ні "або" в залежності). Для більш складних випадків: просто подивіться на такий інструмент, як debian apt або щось подібне.

Нарешті, будівництво вашого автомобіля зводиться до можливих кроків, а CarBuilder дозволяє зрозуміти необхідні кроки.

Однак важливо, що ви повинні знайти спосіб повідомити CarBuilder про всі кроки. Спробуйте це зробити за допомогою конфігураційного файла. Додавання додаткового кроку потребує лише доповнення до конфігураційного файлу. Якщо вам потрібна логіка, спробуйте додати DSL у конфігураційний файл. Якщо все інше не вдається, ви застрягли, визначаючи їх у коді.


0

Кілька речей, які ви згадуєте, виділяються як «погані» для мене

"Моя логіка в даний час використовує Reflection для простоти реалізації"

Для цього справді немає виправдання. Ви дійсно використовуєте порівняння рядків імен методів, щоб визначити, що запустити ?! якщо ви зміните порядок, вам доведеться змінити назву методу ?!

"Операції в одну фазу вже незалежні"

Якщо вони справді незалежні один від одного, це говорить про те, що ваш клас взяв на себе більше ніж одну відповідальність. На мій погляд, обидва ваші приклади, здається, піддаються якомусь синглу

MyObject.AddComponent(IComponent component) 

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

Насправді я думаю, що вони не є справді незалежними, я уявляю, що кожен вивчає стан об'єкта та модифікує його, або принаймні виконує якусь перевірку перед початком.

У цьому випадку ви можете:

  1. Викиньте OOP у вікно та отримайте служби, які працюють на відкритих даних у вашому класі, тобто.

    Service.AddDoorToCar (Автомобільний автомобіль)

  2. Майте клас будівельника, який будує ваш об'єкт, спочатку збираючи необхідні дані та передаючи назад завершений об'єкт, тобто.

    Автомобіль = CarBuilder.WithDoor (). WithDoor (). WithWheels ();

(логіку withX можна знову розділити на підбудівники)

  1. Скористайтеся функціональним підходом та передайте функції / делегати в метод модифікації

    Car.Modify ((c) => c.doors ++);


Юан сказав: "Викиньте OOP у вікно та отримайте служби, які працюють на відкритих даних у вашому класі" --- Як це не орієнтовано на об'єкти?
Грег Бургхардт

?? це не так, це опція, яка не є OO
Ewan

Ви використовуєте об'єкти, це означає, що це "об'єктно-орієнтоване". Тільки тому, що ви не можете визначити шаблон програмування для свого коду, не означає, що він не орієнтований на об'єкти. Принципи SOLID - це хороше правило, яке слід пройти. Якщо ви реалізуєте деякі або всі принципи SOLID, він орієнтований на об'єкт. Дійсно, якщо ви використовуєте об'єкти, а не просто передаєте структури різним процедурам або статичним методам, це об'єктно орієнтоване. Існує різниця між "об'єктно-орієнтованим кодом" та "хорошим кодом". Ви можете написати неправильний об'єктно-орієнтований код.
Грег Бургхардт

це "не OO", тому що ви виставляєте дані так, ніби це була структура та працюєте над нею в окремому класі, як у процедурі
Ewan

tbh Я лише додав провідний коментар, щоб спробувати зупинити неминуче "це не OOP!" коментарі
Ewan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.