Я порушую практику ООП з цією архітектурою?


23

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

Користувальницький інтерфейс (MVC-шаблон), бізнес-логічний шар (BLL) та шар доступу до даних (DAL)

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

Типовим потоком через додаток може бути:

Подія, запущена в інтерфейсі користувача, переходить до методу в BLL, виконує логіку (можливо, в декількох частинах BLL), врешті-решт, в DAL, назад в BLL (де можливо більше логіки), а потім повертає деяке значення в інтерфейс користувача.

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

введіть тут опис зображення

Версія праворуч - це мої зусилля.

Логіка ще як потоки додатків між UI і DAL, але не можливо ніяких властивостей ... Тільки методи (більшість класів в цьому шарі може можливо бути статичним , оскільки вони не зберігають стан). Шар Poco - це те, де існують класи, які мають властивості (наприклад, клас Person, де було б ім'я, вік, зріст тощо). Вони не мають нічого спільного з потоком програми, вони лише зберігають стан.

Потік може бути:

Навіть спрацьовує з інтерфейсу користувача та передає деякі дані контролеру шару інтерфейсу (MVC). Це переводить необроблені дані та перетворює їх у модель poco. Потім модель poco передається в рівень Logic (який був BLL) і, врешті-решт, в рівень запиту команд, потенційно маніпулюючи по дорозі. Рівень запиту Command перетворює POCO в об'єкт бази даних (майже однакове, але один призначений для стійкості, інший для переднього кінця). Елемент зберігається, а об’єкт бази даних повертається до рівня «Запит запитів команд». Потім він перетворюється в POCO, де він повертається до рівня Logic, потенційно обробляється далі, а потім, нарешті, назад до інтерфейсу користувача

Спільна логіка та інтерфейси - це те, де ми можемо мати стійкі дані, такі як MaxNumberOf_X та TotalAllowed_X та всі інтерфейси.

І спільна логіка / інтерфейси, і DAL є "базою" архітектури. Вони нічого не знають про зовнішній світ.

Все знає про poco, крім спільної логіки / інтерфейсів та DAL.

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

Прикладом демонстрації Logic і Poco може бути:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}

Я не бачу нічого принципово неправильного у вашій архітектурі, як ви це описали.
Роберт Харві

19
У прикладі коду недостатньо функціональних деталей для подальшого розуміння. Приклади Foobar рідко дають достатню ілюстрацію.
Роберт Харві

1
Представлено на розгляд: Baruco 2012: Deconstructing the Framework, автор Gary Bernhardt
Theraot

4
Чи можна знайти кращу назву для цього питання , так що можна знайти в Інтернеті легко?
Soner Gönül

1
Просто бути педантичним: ярус і шар - це не одне і те ж. "Рівень" говорить про розгортання, "шар" про логіку. Ваш рівень даних буде розгорнутий як на рівні сервера, так і на коді та рівні бази даних. Ваш рівень користувальницького інтерфейсу буде розгорнуто як на рівні кодів веб-клієнта, так і на стороні сервера. Показана вами архітектура - це тришарова архітектура. Ваші рівні: "Веб-клієнт", "Код сторони сервера" та "База даних".
Лоран LA RIZZA

Відповіді:


54

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

Обґрунтування : Найбільш основна концепція ООП полягає в тому, що дані та логіка утворюють єдину одиницю (об’єкт). Хоча це дуже спрощений і механічний вислів, тим не менш, він не дуже дотримується у вашому дизайні (якщо я вас правильно зрозумів). Ви досить чітко відокремлюєте більшість даних від більшості логіки. Наприклад, такі методи, що не мають статусу (статичні), називаються "процедурами" і, як правило, є протизапальними для OOP.

Звичайно, завжди є винятки, але ця конструкція, як правило, порушує ці речі.

Знову хотілося б наголосити, що "порушує ООП"! = "Неправильно", тому це не обов'язково є оцінним судженням. Все залежить від ваших обмежень архітектури, випадків використання ремонту, вимог тощо.


9
Запросити, це хороша відповідь, якби я писав власний, я скопіював би це і вставлю, але також додаю, що якщо ви знайдете, що не пишете код OOP, можливо, ви повинні розглянути мову, яка не є OOP, як її поставляється з великою кількістю додаткових накладних витрат, які можна обійтися, якщо ви не користуєтесь цим
TheCatWhisperer

2
@TheCatWhisperer: Сучасні архітектури підприємств не відкидають OOP повністю, а вибірково (наприклад, для DTO).
Роберт Харві

@RobertHarvey Погодився, я мав на увазі, якщо ви не використовуєте OOP навряд чи де-небудь у своєму дизайні
TheCatWhisperer

@TheCatWhisperer багато переваг в oop, як c #, не обов'язково в oop частині мови, а в доступній підтримці, такі як бібліотеки, візуальна студія, управління пам’яттю тощо

@Orangesandlemons Я впевнений, що там багато інших добре підтримуваних мов ...
TheCatWhisperer

31

Одним із основних принципів функціонального програмування є чисті функції.

Одним із основних принципів об'єктно-орієнтованого програмування є поєднання функцій із даними, на які вони діють.

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

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

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

Здається, у вашого POCO ділова логіка в них просто чудова, тому я б не переживав надто анемічно. Що мене хвилює - це всі вони здаються незмінними. Пам'ятайте, що геттери та сетери не забезпечують реальної інкапсуляції. Якщо ваш POCO прямує до цієї межі, тоді добре. Просто зрозумійте, що це не дає вам повних переваг реального інкапсульованого об'єкта OOP. Деякі називають це Об'єктом передачі даних або DTO.

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

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

Зараз врешті-решт щось, десь збирається перейти на іншу межу, і все це знову розпадається. Це добре. Закрутіть ще один DTO і киньте його по стіні.

У цьому суть того, про що йдеться в архітектурі портів і адаптерів. Я читав про це з функціональної точки зору . Можливо, це зацікавить і вас.


5
" геттери і сетери не забезпечують реальної інкапсуляції " - так!
Борис Павук

3
@BoristheSpider - геттери і сетери абсолютно пропонують інкапсуляцію, вони просто не відповідають вашому вузькому визначенню інкапсуляції.
Давор Ждрало

4
@ DavorŽdralo: Іноді вони корисні як вирішення, але за своєю суттю геттери та сетери порушують інкапсуляцію. Надання способу отримати та встановити якусь внутрішню змінну - це протилежне відповідальності за власну державу та за її дії.
cHao

5
@cHao - ти не розумієш, що таке геттер. Це не означає метод, який повертає значення властивості об'єкта. Це звичайна реалізація, але вона може повернути значення з бази даних, запросити її через http, обчислити її на льоту, що завгодно. Як я вже говорив, геттери та сетери порушують інкапсуляцію лише тоді, коли люди використовують власні вузькі (і неправильні) визначення.
Давор Ждрало

4
@cHao - інкапсуляція означає, що ви приховуєте реалізацію. Саме це стає інкапсульованим. Якщо у вас є getter "getSurfaceArea ()" для класу Square, ви не знаєте, чи є площа поверхні, якщо вона розрахована на льоту (висота повернення * ширина) або якийсь третій метод, тож ви можете змінити внутрішню реалізацію коли завгодно, тому що він інкапсульований.
Давор Ждрало

1

Якщо я правильно прочитав ваше пояснення, ваші об'єкти виглядають приблизно так: (хитро без контексту)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

У тому, що ваші класи Poco містять лише дані, а ваші класи Logic містять методи, які діють на ці дані; так, ви порушили принципи "Classic OOP"

Знову ж таки, важко сказати з узагальненого опису, але я б загрожував, що те, що ви написали, можна віднести до категорії «Анемічна модель домену».

Я не думаю, що це особливо поганий підхід, і, якщо ви вважаєте, що ваш Poco є структурою, це не викликає злом OOP у більш конкретному сенсі. У тому, що ваші Об'єкти тепер є LogicClasses. Дійсно, якщо ви зробите ваш Pocos незмінним, дизайн може вважатися досить функціональним.

Однак, коли ви посилаєтесь на Shared Logic, Pocos, які майже не є однаковими, і статику я починаю турбуватися про деталі вашого дизайну.


Я додав до своєї публікації, фактично копіюючи ваш приклад. Вибачте, ти не зрозумів для початку
MyDaftQuestions

1
Що я маю на увазі, якщо ви сказали нам, що робить програма, було б простіше писати приклади. Замість LogicClass у вас може бути PaymentProvider або що завгодно
Еван

1

Одна з потенційних проблем, які я бачив у вашому дизайні (і це дуже часто) - деякі з абсолютно гірших кодів "OO", з якими я коли-небудь стикався, були викликані архітектурою, яка відокремлювала об'єкти "Data" від об'єктів "Code". Це речі на рівні кошмару! Проблема полягає в тому, що скрізь у коді вашого бізнесу, коли ви хочете отримати доступ до своїх об'єктів даних, ви РОЗПОЛОЧИЛИ просто кодувати його прямо там в рядку (Вам не потрібно, ви можете створити клас утиліти або іншу функцію, щоб обробити його, але це те, що Я бачив це неодноразово з часом).

Код доступу / оновлення, як правило, не збирається, тому у вас є дублікат функціоналу скрізь.

З іншого боку, ці об'єкти даних є корисними, наприклад, як збереження бази даних. Я спробував три рішення:

Копіювання значень в «істинні» об’єкти та викидання об’єкта даних є стомлюючим (але може бути правильним рішенням, якщо ви хочете піти цим шляхом).

Додавання методів впорядкування даних до об’єктів даних може працювати, але це може зробити великий об'єкт даних безладним, який робить більше ніж одне. Це також може ускладнити інкапсуляцію, оскільки багато механізмів стійкості хочуть публічних приєднань ... Мені це не подобалося, коли я це робив, але це правильне рішення

Найкраще для мене вирішення - це концепція класу "Wrapper", який інкапсулює клас "Data" і містить усі функції функціонування даних - тоді я взагалі не піддаю класу даних (Навіть не сетерам і геттерам) якщо вони абсолютно не потрібні). Це знімає спокусу безпосередньо маніпулювати об'єктом і змушує вас замість цього додавати спільну функціональність у обгортку.

Інша перевага полягає в тому, що ви можете забезпечити, щоб ваш клас даних завжди був у дійсному стані. Ось короткий приклад psuedocode:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

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

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

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


1

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

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


0

Категорія "OOP" набагато більша та абстрактніша, ніж те, що ви описуєте. Це все не хвилює. Він дбає про чітку відповідальність, згуртованість, зв’язок. Тож на рівні, про який ви запитуєте, не має сенсу питати про "практику OOPS".

Це сказало на ваш приклад:

Мені здається, є непорозуміння щодо того, що означає MVC. Ви називаєте свій інтерфейс користувача "MVC", окремо від вашої бізнес-логіки та "резервного" контролю. Але для мене MVC включає весь веб-додаток:

  • Модель - містить дані про бізнес + логіку
    • Рівень даних як деталізація реалізації моделі
  • Перегляд - код інтерфейсу, шаблони HTML, CSS тощо
    • Включає аспекти на стороні клієнта, такі як JavaScript, або бібліотеки веб-додатків на одній сторінці тощо.
  • Контроль - клей на стороні сервера між усіма іншими деталями
  • (Є такі розширення, як ViewModel, Batch і т. Д., В які я не заходжу, тут)

Тут є надзвичайно важливі базові припущення:

  • Клас Model / заперечить ніколи має якісь - або знання взагалі про будь - яких інших частин (View, Control, ...). Він ніколи не викликає їх, не передбачає, що вони викликаються ними, він не отримує ніяких атрибутів / параметрів сесії або нічого іншого по цій лінії. Це зовсім самотньо. На мовах, які підтримують це (наприклад, Ruby), ви можете запустити командний рядок вручну, інстанціювати класи класів Model, працювати з ними до вмісту ваших сердець і можете робити все, що вони роблять, без будь-якого екземпляра Control або View або будь-якої іншої категорії. Вона не має знань про сеанси, користувачів тощо, головне.
  • Ніщо не торкається шару даних, крім моделі.
  • Вигляд має лише легкий дотик до моделі (показ матеріалів тощо) та нічого іншого. (Зауважте, що гарне розширення - це "ViewModel", це спеціальні класи, які здійснюють більш істотну обробку для передачі даних складним способом, яка не вписується ні в модель, ні в перегляд - це хороший кандидат для видалення / уникнення набряку в чиста модель).
  • Контроль є максимально легким, але він несе відповідальність за збирання всіх інших гравців разом, а також передачу матеріалів між ними (тобто вилучення записів користувачів із форми та пересилання її до моделі, пересилання винятків з ділової логіки на корисну повідомлення про помилку для користувача тощо). Для API / Web / HTTP / REST тощо, всі авторизація, безпека, управління сеансами, управління користувачами тощо відбуваються тут (і тільки тут).

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

Зауважте, що "жирні моделі" означають, що вся бізнес-логіка знаходиться в категорії "Модель" (пакет, модуль, незалежно від назви на вашій мові). Окремі класи повинні, очевидно, бути структурованими OOP з хорошим способом за будь-якими вказівками щодо кодування, які ви даєте самі (тобто деякі максимальні рядки коду на клас чи за метод тощо).

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

Щоб повернутися до вашого запитання: мені здається, що між вашою новою архітектурою та схемою MVC, яку я описав, є велике перекриття, тож ви не на абсолютно неправильному шляху, але ви, здається, або винаходите деякі речі, або використовувати його, тому що ваше поточне середовище програмування / бібліотеки пропонують таке. Важко сказати для мене. Тому я не можу дати точну відповідь щодо того, чи є те, що ви маєте намір особливо добре чи погано. Ви можете дізнатись, перевіривши, чи кожна окрема річ відповідає за неї саме одним класом; чи все сильно згуртовано і низько пов'язане. Це дає вам хорошу вказівку, і, на мою думку, достатньо для гарного дизайну ООП (або хорошого еталону того ж, якщо ви хочете).

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