Як зазначається в досить багатьох відповідях та коментарях, ЗНО є доцільним і корисним у деяких ситуаціях, особливо при передачі даних через межі (наприклад, серіалізація в JSON для надсилання через веб-сервіс). У решті цієї відповіді я більш-менш ігнорую це і розповім про доменні класи та про те, як вони можуть бути розроблені таким чином, щоб мінімізувати (якщо не усунути) геттерів та сеттерів, і все ще бути корисним у великому проекті. Я також не буду говорити про те, навіщо видаляти геттери чи сетери, або коли це робити, тому що це питання самі по собі.
Як приклад, уявіть, що ваш проект - це настільна гра на кшталт «Шахи» або «Бойовий корабель». У вас можуть бути різні способи подання цього шару в презентаційному шарі (консольний додаток, веб-служба, графічний інтерфейс тощо), але у вас також є основний домен. Ви можете мати один клас Coordinate
, який представляє позицію на дошці. "Злий" спосіб його написати:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Я буду писати приклади коду в C #, а не на Java, для стислості і тому, що я з ним більше знайомий. Сподіваюся, це не проблема. Концепції однакові, а переклад повинен бути простим.)
Видалення сетерів: незмінність
У той час як громадські жителі та сетери є потенційно проблематичними, вони є набагато "злішими" з двох. Їх також зазвичай простіше усунути. Процес - це просте встановлення значення в конструкторі. Будь-які методи, які раніше мутували об'єкт, повинні натомість повернути новий результат. Тому:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
Зауважте, що це не захищає від інших методів класу, що мутують X та Y. Щоб бути більш непорушним, ви можете використовувати readonly
( final
на Java). Але в будь-якому випадку - чи ви робите свої властивості справді непорушними, або просто запобігаєте прямим мутаціям громадськості через сеттерів - це робить фокус видалення ваших громадських сеттерів. У переважній більшості ситуацій це працює просто чудово.
Видалення геттерів, частина 1: Проектування поведінки
Вищезазначене - це все добре і добре для сетерів, але з точки зору геттерів ми насправді стріляли в ногу ще до того, як стартувати. Наш процес полягав у тому, щоб придумати, що таке координата - дані, які вона представляє - та створити навколо цього клас. Натомість нам слід було почати з того, яка поведінка нам потрібна з координати. Цей процес, до речі, допомагає TDD, де ми витягуємо класи на зразок цього лише після того, як у них з’явиться потреба, тому ми починаємо з бажаної поведінки та роботи звідти.
Тож скажімо, що Coordinate
для виявлення зіткнень перше місце, яке вам знадобилося, було: ви хотіли перевірити, чи дві деталі займають однаковий простір на дошці. Ось "злий" спосіб (конструктори опущені для стислості):
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
І ось хороший спосіб:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
( IEquatable
реалізація скорочено для простоти). Розробляючи поведінку, а не моделюючи дані, нам вдалося видалити наших гетерів.
Зверніть увагу, це також стосується вашого прикладу. Можливо, ви використовуєте ORM або відображаєте інформацію про клієнтів на веб-сайті або щось подібне, і в цьому випадку певний Customer
DTO, мабуть, має сенс. Але те, що ваша система включає клієнтів і вони представлені в моделі даних, не означає автоматично, що вам слід мати Customer
клас у вашому домені. Можливо, коли ви розробляєте поведінку, це з’явиться один, але якщо ви хочете уникнути геттерів, не створюйте його попередньо.
Видалення геттерів, частина 2: Зовнішня поведінка
Таким чином, вище , це хороший початок, але рано чи пізно ви, ймовірно , зіткнутися з ситуацією , коли у вас є поведінка , яке пов'язане з класом, який яким - то чином залежить від стану класу, але не належить по класу. Таке поведінка - це те, що зазвичай живе у службовому рівні вашої програми.
Беручи наш Coordinate
приклад, з часом ви захочете представити свою гру користувачеві, і це може означати малюнок на екрані. Наприклад, у вас може бути проект інтерфейсу, який використовується Vector2
для відображення точки на екрані. Але було б недоцільно, щоб Coordinate
клас взяв на себе відповідальність за перетворення координати в точку на екрані, що принесло б усілякі проблеми презентації у вашій основній області. На жаль, такий тип ситуацій притаманний дизайну ОО.
Перший варіант , який дуже часто обирають, - це просто викрити прокляті геттери і сказати на біса. У цьому є перевага простоти. Але оскільки ми говоримо про те, щоб уникнути заготівлі, скажімо заради аргументу, ми відкидаємо це і бачимо, які існують інші варіанти.
Другий варіант - додати якийсь .ToDTO()
метод у своєму класі. Це - або подібне - цілком може знадобитися в будь-якому випадку, наприклад, коли ви хочете зберегти гру, вам потрібно захопити майже весь ваш стан. Але різниця між тим, що робити це для своїх послуг, і просто звертатися безпосередньо до геттера, є більш-менш естетичною. Це все ще має стільки ж «зла» для нього.
Третім варіантом, який я бачив, як виступає Зоран Горват у кількох відео Pluralsight, - використовувати модифіковану версію шаблону відвідувачів. Це досить незвичне використання та зміна структури, і я думаю, що пробіг у людей сильно відрізнятиметься від того, чи додає це складність без реального виграшу, чи це приємний компроміс для ситуації. По суті, ідея полягає у використанні стандартного шаблону відвідувачів, але мають Visit
методи приймати необхідний стан як параметри замість класу, який вони відвідують. Приклади можна знайти тут .
Для нашої проблеми рішенням із використанням цього шаблону було б:
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
Як ви, напевно, можете сказати, _x
і насправді_y
вже не інкапсульовано. Ми могли б витягти їх, створивши такий, який просто повертає їх безпосередньо. Залежно від смаку, ви можете відчувати, що це робить всю вправу безглуздою.IPositionTransformer<Tuple<int,int>>
Однак, із громадськими організаціями, які працюють із громадськими ресурсами, робити речі неправильно, просто витягуючи дані безпосередньо та використовуючи їх у порушення Tell, Don't Ask . Якщо використовувати цей шаблон, насправді простіше зробити це правильно: коли ви хочете створити поведінку, ви автоматично почнете, створюючи пов'язаний з нею тип. Порушення TDA буде дуже очевидним смердючим і, ймовірно, потребує розробки простішого, кращого рішення. На практиці ці пункти набагато простіше зробити це правильно, OO, ніж "злий" спосіб, який заохочують геттери.
Нарешті , навіть якщо це спочатку не очевидно, насправді можуть бути способи розкрити достатню кількість того, що вам потрібно, як поведінку, щоб уникнути необхідності виставляти стан. Наприклад, використовуючи нашу попередню версію Coordinate
, єдиним публічним членом якої є Equals()
(на практиці це потребує повної IEquatable
реалізації), ви можете написати наступний клас у вашому шарі презентації:
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
Виявляється, можливо, дивно, що вся поведінка, яка нам дійсно потрібна від координати, щоб досягти своєї мети, була перевірка рівності! Звичайно, це рішення пристосоване до цієї проблеми і робить припущення щодо прийнятного використання / продуктивності пам'яті. Це лише приклад, який відповідає саме цій проблематичній області, а не креслення для загального рішення.
І знову ж таки, думки будуть різними залежно від того, на практиці це зайва складність. У деяких випадках такого подібного рішення не може бути або воно може бути надмірно дивним або складним, і в цьому випадку ви можете повернутися до вищевказаних трьох.