×

Чому SOLID — важлива складова мислення програміста. Розбираємося на прикладах з кодом

Привіт! Мене звати Іван, співправцюю з EPAM Systems як Solution Architect, а кар’єру в IT почав 10 років тому. За цей час помітив, що майже всі люблять працювати на проєктах, які починаються з нуля. Та не всім вдається побудувати систему, яку за рік розробки буде все ще легко підтримувати і розвивати. Дехто через кілька місяців робить спробу номер два, оскільки вже знає, як треба було починати правильно. Це природно, що зі зростанням системи зростає і її складність. Успіх розробки такої системи буде залежати від того, наскільки добре ви тримаєте під контролем її складність. Для цього існують дизайн-патерни, найкращі практики, а головне — принципи проєктування, такі які SOLID, GRASP та DDD. У статті хочу звернути увагу на те, що SOLID — це важлива складова мислення розробника, яку потрібно розвивати і тренувати.

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

Навіщо потрібен SOLID

SOLID — це набір принципів об’єктно-орієнтованого програмування, які представив Роберт Мартін (дядько Боб) у 1995 році. Їхня ідея в тому, що треба уникати залежностей між компонентами коду. Якщо є велика кількість залежностей, такий код важко підтримувати (спагеті-код). Його основні проблеми:

  • жорсткість (Rigidity): кожна зміна викликає багато інших змін;
  • крихкість (Fragility): зміни в одній частині ламають роботу інших частин;
  • нерухомість (Immobility): код не можна повторно використати за межами його контексту.

Принцип єдиного обов’язку (Single Responsibility Principle)

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

Наприклад, клас User. Його обов’язок надавати інформацію про користувача: ім’я, email і тип підписки, яку він використовує в сервісі.

enum SubscriptionTypes {
  BASIC = 'BASIC',
  PREMIUM = 'PREMIUM'
}
 
class User {
  constructor (
    public readonly firstName: string,
    public readonly lastName: string,
    public readonly email: string,
    public readonly subscriptionType: SubscriptionTypes,
    public readonly subscriptionExpirationDate: Date
  ) {}
 
  public get name(): string {
    return `${this.firstName} ${this.lastName}`;
  }
 
  public hasUnlimitedContentAccess() {
    const now = new Date();
 
    return this.subscriptionType === SubscriptionTypes.PREMIUM
      && this.subscriptionExpirationDate > now;
  }
}

Розглянемо метод hasUnlimitedContentAccess. На основі типу підписки він визначає, чи є у користувача необмежений доступ до контенту. Але ж стоп, хіба це відповідальність класу User робити такий висновок? Виходить, у класу User є дві мети для існування: надавати інформацію про користувача і робити висновок, який у нього доступ до контенту на основі підписки. Це порушує принцип Single Responsibility.

Чому існування методу hasUnlimitedContentAccess у класі User має негативні наслідки? Бо контроль над типом підписки розпливається по всій програмі. Крім класу User, можуть бути класи MediaLibrary та Player, які теж вирішуватимуть, що їм робити на основі цих даних. Кожен клас трактує по-своєму, що означає тип підписки. Якщо правила наявних підписок змінюються, треба оновлювати всі класи, оскільки кожен вибудував свій набір правил роботи з ними.

Видалимо метод hasUnlimitedContentAccess у класі User і створимо новий клас, який буде відповідати за роботу з підписками.

class AccessManager {
  public static hasUnlimitedContentAccess(user: User) {
    const now = new Date();
 
    return user.subscriptionType === SubscriptionTypes.PREMIUM
      && user.subscriptionExpirationDate > now;
  }
 
  public static getBasicContent(movies: Movie[]) {
    return movies.filter(movie => movie.subscriptionType === SubscriptionTypes.BASIC);
  }
 
  public static getPremiumContent(movies: Movie[]) {
    return movies.filter(movie => movie.subscriptionType === SubscriptionTypes.PREMIUM);
  }
 
  public static getContentForUserWithBasicAccess(movies: Movie[]) {
    return AccessManager.getBasicContent(movies);
  }
 
  public static getContentForUserWithUnlimitedAccess(movies: Movie[]) {
    return movies;
  }
}

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

Single Responsibility Principle стосується не тільки рівня класів — модулі класів теж потрібно проєктувати таким чином, щоб вони були вузько спеціалізовані.

Крім SOLID існує ще інший набір принципів проектування програмного забезепення — GRASP. Деякі його принципи перетинаються з SOLID. Якщо говорити про Single Responsibility Principle, то з GRASP можно співставити:

  • інформаційний експерт (Information Expert) — об’єкт, який володіє повною інформацією з предметної області;
  • низька зв’язаність (Low Coupling) і високе зчеплення (High Cohesion) — компоненти різних класів або модулів повинні мати слабкі зв’язки між собою, але компоненти одного класу або модуля мають бути логічно пов’язані або тісно взаємодіяти один з одним.

Принцип відкритості/закритості (Open/Close Principle)

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

Напевно, кожен з нас бачив нескінченні ланцюжки if then else або switch. Щойно додається чергова умова, ми пишемо черговий if then else, змінюючи при цьому сам клас. Або клас виконує процес з багатьма послідовними кроками — і кожен новий крок призводить до його зміни. А це порушує Open/Close Principle.

Як можна розширювати клас і водночас не змінювати його? Розглянемо кілька способів.

class Rect {
  constructor(
    public readonly width: number,
    public readonly height: number
  ) { }
}
 
class Square {
  constructor(
    public readonly width: number
  ) { }
}
 
class Circle {
  constructor(
    public readonly r: number
  ) { }
}
 
class ShapeManager {
  public static getMinArea(shapes: (Rect | Square | Circle)[]): number {
    const areas = shapes.map(shape => {
      if (shape instanceof Rect) {
        return shape.width * shape.height;
      }
 
      if (shape instanceof Square) {
        return Math.pow(shape.width, 2);
      }
 
      if (shape instanceof Circle) {
        return Math.PI * Math.pow(shape.r, 2);
      }
 
      throw new Error('Is not implemented');
    });
 
    return Math.min(...areas);
  }
}

Як бачимо, додавання нових фігур буде призводити до модифікації класу ShapeManager. Оскільки площа фігури тісно пов’язана із самою фігурою, можна змусити фігури самостійно рахувати свою площу, привести їх до одного інтерфейсу, а тоді передавати їх у метод getMinArea.

interface IShape {
  getArea(): number;
}
 
class Rect implements IShape {
  constructor(
    public readonly width: number,
    public readonly height: number
  ) { }
 
  getArea(): number {
    return this.width * this.height;
  }
}
 
class Square implements IShape {
  constructor(
    public readonly width: number
  ) { }
 
  getArea(): number {
    return Math.pow(this.width, 2);
  }
}
 
class Circle implements IShape {
  constructor(
    public readonly r: number
  ) { }
 
  getArea(): number {
    return Math.PI * Math.pow(this.r, 2);
  }
}
 
class ShapeManager {
  public static getMinArea(shapes: IShape[]): number {
    const areas = shapes.map(shape => shape.getArea());
    return Math.min(...areas);
  }
}

Тепер, якщо у нас з’являться нові фігури, все, що потрібно зробити, — це імплементувати інтерфейс IShape. І клас ShapeManager відразу буде її підтримувати без жодних модифікацій.

А що робити, якщо не можемо додавати методи до фігур? Існують методи, які суперечать Single Responsibility Principle. Тоді можна скористатися шаблоном проєктування «Стратегія» (Strategy): створити множину схожих алгоритмів і викликати їх за певним ключем.

interface IShapeAreaStrategiesMap {
  [shapeClassName: string]: (shape: IShape) => number;
}
 
class ShapeManager {
  constructor(
    private readonly strategies: IShapeAreaStrategiesMap
  ) {}
 
  public getMinArea(shapes: IShape[]): number {
    const areas = shapes.map(shape => {
 
      const className = shape.constructor.name;
      const strategy = this.strategies[className];
 
      if (strategy) {
        return strategy(shape);
      }
 
      throw new Error(`Could not find Strategy for '${className}'`);
      
    });
 
    return Math.min(...areas);
  }
}
 
// Strategy Design Pattern
const strategies: IShapeAreaStrategiesMap = {
  [Rect.name]: (shape: Rect) => shape.width * shape.height,
  [Square.name]: (shape: Square) => Math.pow(shape.width, 2),
  [Circle.name]: (shape: Circle) => Math.PI * Math.pow(shape.r, 2)
};
 
const shapes = [
  new Rect(1, 2),
  new Square(1),
  new Circle(1),
];
 
const shapeManager = new ShapeManager(strategies);
console.log(shapeManager.getMinArea(shapes));

Перевага Strategy в тому, що є змога змінювати в рантаймі набір стратегій і спосіб їх вибору. Можна прочитати файл конфігурацій (.json, .xml, .yml) і на його основі збудувати стратегії. Тоді, якщо відбувається зміна стратегій, не потрібно розробляти нову версію програми і деплоїти її на сервери, достатньо підмінити файл з конфігураціями і сказати програмі, щоб та його знову прочитала. Крім того, стратегії можна реєструвати в Inversion of Control контейнері. У такому разі клас, який їх потребує, отримає стратегії автоматично на етапі створення.

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

class ImageProcessor {
...
  public processImage(bitmap: ImageBitmap): ImageBitmap {
    this.fixColorBalance(bitmap);
    this.increaseContrast(bitmap);
    this.fixSkew(bitmap);
    this.highlightLetters(bitmap);
 
    return bitmap;
  }
}

Застосуємо дизайн-патерн «Конвеєр» (Pipeline).

type PipeMethod = (bitmap: ImageBitmap) => void;
 
// Pipeline Design Pattern
class Pipeline {
  constructor(
    private readonly bitmap: ImageBitmap
  ) { }
 
  public pipe(method: PipeMethod) {
    method(this.bitmap);
  }
 
  public getResult() {
    return this.bitmap;
  }
}
 
class ImageProcessor {
  public static processImage(bitmap: ImageBitmap, pipeMethods: PipeMethod[]): ImageBitmap {
    const pipeline = new Pipeline(bitmap);
    pipeMethods.forEach(method => pipeline.pipe(method))
 
    return pipeline.getResult();
  }
}
 
const pipeMethods = [
  fixColorBalance,
  increaseContrast,
  fixSkew,
  highlightLetters
];
 
const result = ImageProcessor.processImage(scannedImage, pipeMethods);

Тепер, якщо потрібно змінити спосіб обробки зображення, ми модифікуємо масив з методами. Сам клас ImageProcessor залишається незмінним. Тепер уявіть, що треба обробляти різні зображення по-різному. Замість того, щоб писати різні версії ImageProcessor, по-іншому скомбінуємо в масиві pipeMethods потрібні нам методи.

Ще кілька переваг. Раніше ми додавали новий метод обробки зображення прямо в ImageProcessor, і в нас виникала потреба додавати нові залежності. Наприклад, метод highlightLetters вимагає додаткову бібліотеку для пошуку символів на зображенні. Відповідно, більше методів — більше залежностей. Зараз кожен PipeMethod можна розробити в окремому модулі й підключати тільки необхідні залежності. Після такої декомпозиції все дуже легко тестувати. Ну й на останок: така структура коду мотивує розробника писати якомога коротші методи обробки з чіткими інтерфейсами. До цього можна було зробити один великий метод fixQuality, де б відбувалося і виправлення балансу кольорів, і вирівнювання зображення, і збільшення контрасту. Але в такому великому методі було б складно контролювати параметри кожного накладеного на зображення фільтру. Ймовірно, виникла б ситуація, коли fixQuality працював би добре для одного зразка зображення, але для іншого на етапі тестування він би не працював зовсім. Маючи кілька добре програнульованих методів, значно простіше скоригувати параметри, щоб отримати потрібний результат.

Принципами GRASP, що спільні з Open/Close Principle :

  • стійкість до змін (Protected Variations): потрібно захищати компоненти від впливу змін інших компонентів. Тому для компонентів, які потенційно часто будуть зазнавати змін, ми створюємо один інтерфейс і кілька його імплементацій, використовуючи поліморфізм;
  • поліморфізм (Polymorphism) — можливість мати різні варіанти поведінки на основі типу класу. Типи класу з варіативною поведінкою мають підпадати під один інтерфейс;
  • перенаправлення (Indirection): слабка зв’язаність між компонентами та можливість їх перевикористання досягається завдяки створенню посередника (mediator), який бере на себе взаємодію між компонентами.
  • чиста вигадка (Pure Fabrication): можна створити штучний об’єкт, якого немає в домені, але який наділений властивостями, що дають змогу зменшити залежність між об’єктами. Наприклад, в домені є товар і склад. Якщо зробимо так, що склад буде контролювати наявність товарів, буде складно створити функціонал, який, наприклад, перевірятиме наявність товару в партнерів і пропонуватиме його користувачу. Тому ми додаємо об’єкт ProductManager, який перевірятиме, чи є товар на складі. Якщо немає, перевірятиме його в партнерів. Оскільки за допомогою ProductManager ми відв’язали товар від складу, можемо повністю позбутися його та продавати товари від партнерів, якщо виникне така потреба.

Принцип підстановки Лісков (Liskov Substitution Principle)

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

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

class BaseClass {
  public add(a: number, b: number): number {
    return a + b;
  }
}
 
class DerivedClass extends BaseClass {
  public add(a: number, b: number): number {
    throw new Error('This operation is not supported');
  }
}

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

class Vehicle {
  accelerate() {
    // implementation
  }
 
  slowDown() {
    // implementation
  }
 
  turn(angle: number) {
    // implementation
  }
}
 
class Car extends Vehicle {
}
 
class Bus extends Vehicle {
}

Все працює до того моменту, допоки ми не додамо новий клас Поїзд.

class Train extends Vehicle {
  turn(angle: number) {
    // is that possible?
  }
}

Оскільки поїзд не може змінювати довільно напрямок свого руху, то turn батьківського класу буде порушувати принцип підстановки Лісков.

Щоб виправити цю ситуацію, ми можемо додати два батьківські класи: FreeDirectionalVihicle — який буде дозволяти довільний напрямок руху і BidirectionalVehicle — рух тільки вперед і назад. Тепер всі класи будуть наслідувати тільки ті методи, які можуть забезпечити.

class FreeDirectionalVehicle extends Vehicle {
  turn(angle: number) {
    // implementation
  }
}
 
class BidirectionalVehicle extends Vehicle {
}

Крім того, клас-нащадок не має додавати ніяких умов до виконання методу і після виконання методу. Наприклад:

class Logger {
  log(text: string) {
    console.log(text);
  }
}
 
class FileLogger extends Logger {
  constructor(private readonly path: string) {
    super();
  }
 
  log(text: string) {
    // append text file
  }
}
 
class TcpLogger extends Logger {
  constructor(private readonly ip: string, private readonly port: number) {
    super();
  }
 
  log(text: string) {
    // implementation
  }
 
  openConnection() {
    // implementation
  }
 
  closeConnection() {
    // implementation
  }
}

У цій ієрархії ми не зможемо легко замінити об’єкти батьківського класу FileLogger об’єктами TcpLogger, оскільки до та після виклику методу нам потрібно додатково викликати openConnection та closeConnection. Виходить, ми накладаємо 2 додаткові умови на виклик методу log, що також порушує принцип підстановки Лісков.

Щоб вирішити ситуацію вище, ми можемо зробити методи openConnection та closeConnection приватними. В методі log класу TcpLogger організуємо запис логів в файл. Періодично (наприклад, кожної хвилини) відкриватимемо з’єднання, відправлятимемо файл з логами і закриватемо з’єднання. Додатково потрібно переконатися, що перед тим, як програма буде закрита, ми відправили всі логи. Якщо програма була аварійно завершена, ми можемо відправити логи під час наступного її запуску.

Принцип розділення інтерфейсу (Interface Segregation Principle)

Краще, коли є багато спеціалізованих інтерфейсів, ніж один загальний. Маючи один загальний інтерфейс, є ризик потрапити в ситуацію, коли похідний клас логічно не зможе успадкувати якийсь метод. Розглянемо приклад:

interface IDataSource {
  connect(): Promise<boolean>;
  read(): Promise<string>;
}
 
class DbSource implements IDataSource {
  connect(): Promise<boolean> {
    // implementation
  }
 
  read(): Promise<string> {
    // implementation
  }
}
 
class FileSource implements IDataSource {
  connect(): Promise<boolean> {
    // implementation
  }
 
  read(): Promise<string> {
    // implementation
  }
}

Оскільки з файлу ми читаємо локально, то метод Connect зайвий. Розділимо загальний інтерфейс IDataSource:

interface IDataSource {
  read(): Promise<string>;
}
 
interface IRemoteDataSource extends IDataSource {
  connect(): Promise<boolean>;
}
 
class DbSource implements IRemoteDataSource {
}
 
class FileSource implements IDataSource {
}

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

Принцип інверсії залежностей (Dependency Inversion Principle)

Він складається з двох тверджень:

  • високорівневі модулі не повинні залежати від низькорівневих. І ті, і ті мають залежати від абстракцій;
  • абстракції не мають залежати від деталей реалізації. Деталі реалізації повинні залежати від абстракцій.

Розберемо код, який порушує ці твердження:

class UserService {
  async getUser(): Promise<User> {
    const now = new Date();
 
    const item = localStorage.getItem('user');
    const cachedUserData = item && JSON.parse(item);
 
    if (cachedUserData && new Date(cachedUserData.expirationDate) > now) {
      return cachedUserData.user;
    }
 
    const response = await fetch('/user');
    const user = await response.json();
 
    const expirationDate = new Date();
    expirationDate.setHours(expirationDate.getHours() + 1);
 
    localStorage.setItem('user', JSON.stringify({
      user,
      expirationDate
    }));
 
    return user;
  }
}

Наш модуль верхнього рівня UserService використовує деталі реалізації трьох модулів нижнього рівня: localStorage, fetch та Date. Такий підхід поганий тим, що якщо ми, наприклад, вирішимо замість fetch користуватися бібліотекою, яка робить HTTP-запити, то доведеться переписувати UserService. Крім того, такий код важко покрити тестами.

Ще одним порушенням є те, що з методу getUser ми повертаємо реалізований клас User, а не його абстракцію — інтерфейс IUser.

Створимо абстракції, з якими було б зручно працювати всередині модуля UserService.

interface ICache {
  get<T>(key: string): T | null;
  set<T>(key: string, user: T): void;
}
 
interface IRemoteService {
  get<T>(url: string): Promise<T>;
}
 
class UserService {
  constructor(
    private readonly cache: ICache,
    private readonly remoteService: IRemoteService
  ) {}
 
  async getUser(): Promise<IUser> {
    const cachedUser = this.cache.get<IUser>('user');
 
    if (cachedUser) {
      return cachedUser;
    }
 
    const user = await this.remoteService.get<IUser>('/user');
    this.cache.set('user', user);
 
    return user;
  }
}

Як бачимо, код вийшов значно простішим і його можна легко протестувати. Тепер поглянемо на реалізацію інтерфейсів ICache та IRemoteService.

interface IStorage {
  getItem(key: string): any;
  setItem(key: string, value: string): void;
}
 
class LocalStorageCache implements ICache {
  private readonly storage: IStorage;
 
  constructor(
	getStorage = (): IStorage => localStorage,
	private readonly createDate = (dateStr?: string) => new Date(dateStr)
  ) {
	this.storage = getStorage()
  }
 
  get<T>(key: string): T | null {
	const item = this.storage.getItem(key);
	const cachedData = item && JSON.parse(item);
 
	if (cachedData) {
  	const now = this.createDate();
 
  	if (this.createDate(cachedData.expirationDate) > now) {
    	return cachedData.value;
  	}
	}
 
	return null;
  }
 
  set<T>(key: string, value: T): void {
	const expirationDate = this.createDate();
	expirationDate.setHours(expirationDate.getHours() + 1);
 
	this.storage.setItem(key, JSON.stringify({
  	value,
  	expirationDate
	}));
  }
}
 
class RemoteService implements IRemoteService {
  private readonly fetch: ((input: RequestInfo, init?: RequestInit) => Promise<Response>)
 
  constructor(
	getFetch = () => fetch
  ) {
	this.fetch = getFetch()
  }
 
  async get<T>(url: string): Promise<T> {
	const response = await this.fetch(url);
	const obj = await response.json();
 
	return obj;
  }
}

Ми зробили врапери над localStorage та fetch. Важливим моментом у реалізації двох класів є те, що ми не використовуємо localStorage та fetch прямо. Ми весь час працюємо зі створеними для них інтерфейсами. LocalStorage та fetch будуть передаватися в конструктор, якщо там не буде вказано жодних параметрів. Для тестів можна створити mocks або stubs, які замінять localStorage або fetch, і передати їх як параметри в конструктор.

Схожий прийом використовують і для дати: якщо нічого не передати, то кожного разу LocalStorageCache буде отримувати нову дату. Якщо ж для тестів потрібно зафіксувати певну дату, треба передати її в параметрі конструктора.

Висновки

Це природно, що з розвитком системи зростає її складність. Важливо завжди тримати цю складність під контролем. Інакше може виникнути ситуація, коли додавання нових фіч, навіть не дуже складних, обійдеться занадто дорого. Деякі проблеми повторюються особливо часто. Щоб їх уникати, було розроблено принципи проєктування. Якщо будемо їх дотримуватися, то не допустимо лавиноподібного підвищення складності системи. Найпростішими такими принципами є SOLID.

І на останок: Роберта Мартіна вважають Rock Star у розробці програмного забезпечення. На його книгах вже виросло декілька поколінь суперуспішних програмістів. «Clean Code» і «Clean Coder» — дві його книги про те, як писати якісний код і відповідати найвищим стандартам в індустрії

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось17
До обраногоВ обраному41
LinkedIn

Схожі статті




Найкращі коментарі пропустити

Стоит сказать, что проблемы с которыми борются Open/Close, Liskov Substitution и даже в определённых случаях SRP, порождены во многом ограничениями системы типов некоторых ООП языков, которые навязывают полиморфизм через наследование. И как альтернативу предлагают только примитивный subtyping на интерфейсах, но при этом не дают это делать ретроактивно (в расширении, например), а заставляют указывать родство типов прямо в декларации, как у автора в примере.

При наличии статической расширяемости типов, параметрического полиморфизма или ad-hoc полиморфизма с ранним связыванием; алгебраических типов данных и функций высшего порядка — едва ли кому-то придёт в голову применять наследование или модифицировать реализацию кроме ситуаций, когда это действительно необходимо.

Иными словами, иногда более правильным решением будет смена языка программирования.

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

118 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

принцип Лісков — про властивості і поведінку класів, а не про методи (операції) інтерфейсу. Не варто пробувати пояснити контракт класу через його інтерфейс, бо вони не тотожні. До того ж, методи можуть бути опціональними, якщо про це явно оголошено в контракті (наприклад, add/set/remove в java.util.ListIterator).

„клас-нащадок не має додавати ніяких умов до виконання методу і після виконання методу”
нащадок може послаблювати передумову і підсилювати післяумову та
інваріант: „Subclasses in an inheritance hierarchy are allowed to weaken preconditions (but not strengthen them) and strengthen postconditions and invariants (but not weaken them).”
en.wikipedia.org/wiki/Design_by_contract
Наприклад, в Java метод нащадка може повертати підтип результату перевизначеного ним метода базового класу: „The return value of a method of the subclass needs to comply with the same rules as the return value of the method of the superclass. You can only decide to apply even stricter rules by returning a specific subclass of the defined return value, or by returning a subset of the valid return values of the superclass.”
stackify.com/...​ov-substitution-principle

щодо принципу Open/Closed:
Ви пишете, що код класу має залишатись незмінним. Власне, це вимога застарілого принципу Меєра, щоб спростити і пришвидшити внесення змін в умовах, коли неможливі належні тестування і перевірка. Одночасно він додає зайвий рівень наслідування і суперечить принципу інкапсуляції, який допускає і заохочує заміну реалізації класу при потребі.
Сучасний принцип Мартіна вимагає використання абстрактних типів (інтерфейсів) і збереження контракту класу, а не його коду.
stackify.com/...​gn-open-closed-principle
«It uses interfaces instead of superclasses to allow different implementations which you can easily substitute without changing the code that uses them. The interfaces are closed for modifications, and you can provide new implementations to extend the functionality of your software.»

Сучасний принцип Мартіна вимагає використання абстрактних типів (інтерфейсів) і збереження контракту класу, а не його коду.

А яка принципова різниця? Контракт теж може змінюватись під нові потреби, якщо щось у старому починає бути неадекватним. Наприклад, додати ще параметрів, чи переробити createFoo() на створення проміжного builderʼа...
Можна, зрозуміло, зробити інший інтерфейс, і деякий час підтримувати обидва, але це все одно проміжне рішення.

Казалось бы, разве мало таких книг и статей понаписано за годы?
Причем каждая обычно вызывает холивары. Если толковая, а не педагогическая, для студентов.

Причина ж вечности темы, как по мне, в том что код читается — разными людьми.
Он не отражает той цели что декларируется — рассказать о принципах, показать способ мышления,
а является всего лишь как пример отца архитектора складывающего из кубиков домик, и поясняющего 6летнему сыну — вот видишь, это стиль ампир, а вот видишь, это модерн. О, а если мы добавим пару кубиков вот сюда, — это будет эклектика из барокко.
Все понял?

Ну, 14летний подойдет и конечно затребует формализацию. он уже умный, все понял.

Вот и будет конструктивное обсуждение :D

Принцип підстановки Лісков (Liskov Substitution Principle)

Лісков — це єдине прізвище в списку принципів SOLID, ну можна було б хоча б пару слів про неї написати, і чому цей принцип назвали на її честь.

Так, можливо б дійсно мало сенс

ну можна було б хоча б пару слів про неї написати

uk.wikipedia.org/wiki/Барбара_Лісков

Класи мають бути відкриті до розширення, але закриті для змін

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

Рефакторинг — це не щось дуже хороше, що немає недоліків. Рефакторинг має певну ціну — регрешен тестування, додатковий час розробника на покращення коду, додатковий час інших розробників на code review, додатковий час на виправлення потенційних багів. Всі покращення, які ви робите пізніше, вони обходяться дорожче. Рефакторінг виступає як необхідність, а не щось таке, що буде робити розробник, бо у нього зв’явився вільний час. Саме визначення рефакторингу каже про певні проблеми з кодом. Якщо вим вдалось відразу спроектувати правильний код, ви рефакторингу робити не будете. Рефакторинг — це дорого, тому і почали говорити про найкращі практики і підходи до проектування, щоб його максимально уникати.

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

але закриті для змін

не означає «але для рефакторінгу можна». Ви змінили код і не завжди можете точно спрогнозувати наслідки. Code Review і повторне тестування мають певну ціну.

Вибачте, але ви себе позиціонуєте як Solution Architect, тобто позиція, яка вимагає і знань, і досвіду, але я знову з вами не погоджуся.

Якщо вим вдалось відразу спроектувати правильний код, ви рефакторингу робити не будете.

По-перше, проектують НЕ код, а дизайн або структуру. Але справа не тільки в термінології. Якщо ви працюєте в живому проект, то в ньому постійно змінюються або додаються вимоги. Якщо у вас змінилася існуюче вимога, ви ж не будете на кожен чих плодити новий клас. Ви будете міняти код, а іноді і рефакторити, якщо наприклад щось не підходить. В цьому і є принцип Agile — зробити свою систему гнучкою для змін. Боятися цього можна тільки якщо у вас немає покриття тестами. Але відсутність тестами це вже явний анти-патерн.

В цьому і є принцип Agile — зробити свою систему гнучкою для змін

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

Якщо у вас змінилася існуюче вимога, ви ж не будете на кожен чих плодити новий клас

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

Дякую за статтю, але є кілька коментарів

уникати залежностей між компонентами коду

Уникати залежностей між класами неможливо, вони що, у вас все висітимуть в повітрі? Йдеться про те, що потрібно уникати tight coupling, і намагатися використовувати loose coupling

Йдеться не тільки про loose coupling, але і зменшення кількості самих залежностей. Декілька способів, як зменшити кількість залежностей

— Дизайн Патерн Фасад — за Фасадом можна сховати взаємодію декількох компонентів системи, замість того, щоб додавати ці компоненти як залежності до нашої компоненти. Замість залежності від N компонентів ми маємо лише залежність на 1 компоненту — Фасад. Переваги такого підходу: Фасад можна перевикористати; наша компонента стає простішою, її лекше спеціалізувати на виконанні тільки одного обов’язку; якщо компоненти за Фасадом вимагають змін, то зміни зачеплять лише Фасад.

— Дизайн Патерн Посередник — взаємодія між компонентами відбувається через Посередника, який приймає дані від одної з компонент і вирішує, яка наступна компонента має виконати задачу. Компоненти не взаємодіють напряму одна з одною, всі компоненти залежні лише на Фасад.

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

— Подібно до Стратегія можна сказати і про Конвеєр.

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

Якщо в loose coupling ви намагаєтесь зробити так, щоб один компонент взаємодіяв з іншим, але не сильно знав про цей компонент і не залежав від його реалізації, то у випадку зменшення кількості залежностей ви намагаєтесь зробити сам компонент простішим, понизити його складністі за рахунок інтеграції або комунікації з меншою кількісті залежностей. Я десь читав, може в тому ж Clean Code, що в конструкторі не бажано мати більше, ніж 3 аргументи (мається на увазі, що у вас IoC і ви не створююєте залежності в коді через new). Тобто десь в такому діапазоні варто тримати порядок залежностей, щоб легко контролювати ріст складності.

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

більшість патернів — це ж про те, як зв’язуватися класи і інтерфейси

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

Якщо ви боїтеся зв’язків, значить, у вас якісь фундаментальні прогалини в плані ООП.

Про пониження порядку зв’язків говорять не тільки патерни. В Domain Driven Design є поняття Bounded Context. У ньому ви ізолюєте бізнес моделі, які відносяться до конкретного субдомену. Моделі різних Bounded Context не можуть спілкуватись на пряму. Якщо модель відноситься до декількох Bounded Context одночасно, то спеціалізована копія моделі буде присутня у всіх Bounded Context до яких вона має відношення. Ще є понняття Aggragate. Це група споріднених моделей, які мають тісні зв’язки між собою і які можна сприймати як одну функціональну одиницю. Aggratege має Aggrate Root — тільки до цієї моделі можна звертатись поза межами Aggratege і вона здійснює оркестрацію інших моделей всередині Aggratege.
Велика кількість зв’язків є джерелом неконтрольованої складності системи. Про це багато писали Мартін Фаулер та Ерік Еванс.
Як ви думаєте, чому Microservices стали такі популярні? З ними складність деплойменту суттєво виросла, отже мала бути дуже суттєва перевага, щоб компенсувати витрати на складний деплоймент. Цією перевагою було якраз фізичне розділення між компонентами, які не спорідненні.

Дякую, все просто та чудово написано! Я б ще порекомендувала прочитати «Clean Architecture» Боба, взагалі відмінна книга!

В українській мові немає такого слова "

Спасибі

", в суржику є, а в українській мові немає. Навіщо спотворювати таку гарну мову?

В українській мові немає такого слова "

Спасибі
«, в суржику є, а в українській мові немає

Ніт, слово є і не суржик. Інша історія, що «дякую» більш вживане.

В Кобзарі я нарахував 23 спасибі і 2 дякую.

Не бійтесь заглядати у словник ©
sum.in.ua/s/spasybi

Крутяк! Дякую за статтю :)

It’s probably time to stop recommending Clean Code qntm.org/clean

твиты по этому поводу: twitter.com/...​org/clean&src=typed_query

Процитирую Alex Nedelcu @alexelcu:

Nice negative review of „Clean Code”, the book. I always wondered how some books keep being recommended. Do people actually read them?

Не советую даже читать этот мусор. Автор этого блога — психически больной SJW левак, который решил обосрать Мартина чисто из за политических разногласий. Каких-либо аргументов он там не привел, кроме как поржал с какого-то вырванного из контекста кода, который был приведен в книжке Clean Code 2008 года.

Я до блогу ще не дійшов, щоб почитати і зробити свій висновок, але я переглянув твіти. Автор цього коменту посилається на твіт, у якого тільки 20 лайків. Такий собі проф. В інших твітах дуже багато «мені не подобається», «Мартін <&(&%^$%> людина, а людину не можна відокремлювати від книжки», «Мартін ретвітнув Трампа, shame!». Якусь конструктивну критику важко побачити. Я з цікавості загуглив top software programming books 2020. Майже кожне посилання у видачі згадує або Clean Code, або Clean Coder.
Програмісти не народжуються з досвідом програмування і дизайном програмного коду. Ми ще не живемо в матриці, де можна всунути в голову флешку і ти вже вмієш писати правильно складний код. Можна вчитися в колег, можна вчитися на Stack Overflow, але чи завжди ми щось хороше звідти почерпнемо? Чи буде у нас цілісна картинка світу, чи тілько того, з чим доводилось працювати? Про Clean Code дуже багато говорять. Звісно не треба сприймати книжку як святий грааль тільки через те, що про неї багато говорять. Але варто її прочитати і зробити для себе якісь висновки. Мабуть вона ж не просто стілки разів була рекомендована. Там описані досить фундаментальні речі. Тренди 2020 аля кубернетіс, серверлес, лябди і т.д. не сильно спасуть від поганого дизайну коду.

Вполне согласен с дядькой.

Вы просто гляньте вокруг, тут собрался народ с опытом в 10-20 лет в разработке, и не могут прийти к какому-то чёткому единому мнению по теме.

Принципы дизайна кода, должны быть формальными.
Вот термин side-effect имеет право на существование, вещь формальная-чёткая, легко измеримая.

Вот еще довольно любопытные статьи, если кому интересно подробнее узнать про SRP, Open/Closed и про организацию когда в проекте в целом:
Meanwhile... on the command side of my architecture
Meanwhile... on the query side of my architecture

Пример c pipeline (chain of responsibility) приводится некорректно — в этом паттерне должен быть short circuit, у вас его нет. Судя по описанию вам бы лучше подошёл builder паттерн.
И еще — Вы объясняете принципы из ООД и ГоФ паттерны используя не ООП код с нарушениями базовых принципов ооп — пример со стратегией и коллекцией мепов в частности. Ваш код немного подправить и можно объяснять, как использовать функторы в императивном коде.

Хорошая статья.
Солид сам по себе очень срачная тема, но статья хорошая.

Жаль что по DDD еще никто статью не написал.

Как раз не жаль, а к счастью. Эванса достаточно вдумчиво прочесть и потом отрефлексировать как следует, опираясь на опыт (свой и чужой). Нет там никакой высокой науки, чтоб в статьях мусолить. А в солиде и подавно. Есть базовые принципы системного дизайна (coupling, cohesion и несколько ключевых производных от них с поправкой на парадигму), и пока их своими руками и мозгами не перемусолишь лично, никакие статью не помогут. Весь тот текстовый шум, который накопился на эти темы, только мешает и вводит в заблуждение неокрепшие умы. И шум все растёт и растёт. Хватит (плохого) текста. (А хороший никто и не пишет, т.к. почти нереально: невозможно написать статью о том, как ловко ездить на коньках или играть джаз.)

Нет там никакой высокой науки, чтоб в статьях мусолить. А в солиде и подавно.
Солид сам по себе очень срачная тема
ока их своими руками и мозгами не перемусолишь лично

Именно поэтому статьи нужны.
Потому что нет стандартов, критерии достаточно субъективны и спорны.
Потому что кто-то «перемусолили руками» и наработал свое виденье, которое может не совпадать с видением других людей, которые тоже считают что поняли, потому что «перемусолили руками».

Поэтому лично мне всегда очень интересно послушать что-то про солид от другого человека.

Я би й про S окрему статтю запиляв, бо його майже всі розуміють не так як писав сам Мартіна.
В Мартіна там навіть приклад є, щоб пояснити про single reason of change, саме як про єдиного стейкхолдера певної функціональності, коли облік відпрацьованого часу і нарахованих відпусток веде один код, а використовують його одночасно фінанси та адміністративний відділ і в них рано чи пізно виникає тут конфлікт, саме через те, що область ніби та ж сама, а потреби різні, а відтак і бізнес-логіка різна, часом навіть суперечлива. І от для того і є S, щоб різні потреби покривав різний код, який можна по-різному міняти, оскільки як ти те нарахування не спрощуй, із класом, який одночасно старається для бухгалтерії та HR, без костилів не обійтися.

Сам Мартін і не розуміє в першу ж чергу. Якби розумів, не сталося б такого масового нерозуміння, бо описав би одразу як треба, а не писав через двадцять років до-пояснення, що він мав на увазі. SRP неможливо просто так пояснити, бо не існує такого стабільного явища як «єдиний обов’язок». Критеріїв нема, за якими обов’язки можна диференціювати. Біда сталася тоді, коли з’явилось слово «single». І ніякі простенькі приклади й аналогії про стейкхолдерів не можуть пояснити таке складне явище, тому що природа предметних областей, які ми програмуємо, дуже складна, і завжди балансуємо між автономністю і інкапсуляцією. Дизайн — не дискретний, щоб його можна запхати в рамки «один, два, три».

Сам Мартін і не розуміє в першу ж чергу. Якби розумів,

Ему книги и консалтинг продавать надо, так что не важно что он там понимает, пока бабки мутятся :)

К дяде Бобу вопросов нет, каждый крутится, как может, и он выбрал далеко не самый аморальный способ лол, поди не Роберт Киосаки. Но омг поддерживать весь этот пиетет в статейках в 2020-м 🤦‍♂️

Я б не наважився отакво погрожувати південному Дяді Бобу, попиваючи смузі в себе на галері, але думка цікава :-)

ахаха, автор принципу Лисков посвятил пару строк, а открытости/закрытости — пол статьи, видимо кто-то сам плавает в солиде

Тоже считаю, что Open/Closed стоило отдельную статью посвятить.

Автор не книгу собрался писать.
Так-то каждой букве солида можно статью посвятить

ну, судя по размеру описания open/close принципа, как раз так книгу и собирался писать

Стаття написана виходячи зі свого досвіду Code Review та співбесід. Про Liskov Substitution переважно відповідають всі і про підводні камені також знають переважно всі. Open/Close крім поліморфізму ніхто нічого не каже. От ніхто. Коли починаєш наводити якісь приклади, чи самому підштовхувати до відповіді, то так, люди починають замислюватись і генерують якісь відповіді. Така ж ситуація і на код рев’ю: коли запитуєш, як ти плануєш розширяти код через тиждень, коли буде ось ця і ось ця фіча, то всі відразу бачать проблему. Але задетектати її наперед не всім вдається і ще крім того, розширяти без наступних змін класу — часто це буває челендж.
Як бачу Liskov Substitution викликав багато інтересу. На вихідних тоді попрацюю над розширеною версією.

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

YAGNI?
Окрім того, хіба девелопери мають повну картину проекта та планів, чи думати наперед, що буде з проектом — то робота архітектора та лідів?

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

Може і YAGNI. Все залежить від складності. Проект на 2 місяці і на 2 розробники не такий самий, як проект на рік і на 10 розробників. Це я дуже умовно прив’язав кількість часу і кількість розробників до складності проекту. SOLID, GRASP, DDD, шаблони проектування — це все про контроль складності. Впроваження цих практик в проект не дається безкоштовно. Я скажу дуже умовно так: якість архітектури — це довгострокова інвестиція. Якщо ви бачите, що через півроку буде складно, ви на початку проекту інвестуєте час і ресурси в розробку аріхтектури. Ви умовно тратите місяць на дизайн, на PoC, на приклади коду, на документування best practicies, на підготовку команди і так далі. Це може бути дуже сумнівна делівері в очах замовника, бо так ви можете не зробити навіть жодного скріна. Зате, якщо правильний фундамент закладений, пройде рік, а ви спокійно і прогнозовано додаєте нові фічі.

Окрім того, хіба девелопери мають повну картину проекта та планів,

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

чи думати наперед, що буде з проектом — то робота архітектора та лідів?

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

Ви умовно тратите місяць на дизайн, на PoC, на приклади коду, на документування best practicies, на підготовку команди і так далі.

... и коменда потом пишет что попало как попало :)
СОЛИД и тд не получится внедрить за месяц, надо несколько лет готовить людей, если сильно повезет то интенсив на 6-12 месяцев.
Тут проблема в том что разработчики должны __понимать__ зачем это надо, а не просто знать.

... и коменда потом пишет что попало как попало :)

для цього є ви, щоб допомогти писати краще :) ніхто не каже, що за місяць ваша команда, яка до того не сильно вникала в принципи солід, почне по них писати код. Тіммейти знаходять помилки в код рев’ю. Обговорюють їх і доходять до спільного знаменника. Розвивається проект і розвивається команда з проектом. На початку проекта після того, як ви означили архітектуру, вам потрібно зробити з командою шаблони коду. Дуже важливо, щоб цей код був однорідний. В шаблонах на скільки можливо застосувати SOLID. Ви дуже правильно підмітили, що

Тут проблема в том что разработчики должны __понимать__ зачем это надо, а не просто знать.

Розуміння приходить з часом. Не всі з однаковою швидкістю зрозуміють, «чому цей клас так дивно написаний». Але оскільки ви домовились працювати по цих шаблонах, то всі їх будуть фоловити. Це вас трохи вбереже від серйозних помилок в дизайні коду. Потім на код рев’ю ви будете обговорювати різні ситуації і мало помало розуміння прийде.
Важливо, щоб всі розуміли, що ми пишемо код по певним шаблонам не тому, що так треба, або мені сказали. А тому, що команда так вирішила спільно і це спільна відповідальність. Коли робите шаблони, то пояснюйте на прикладах, щоб команда якомога глибше могла зрозуміти, чому так і погодитися з цим. Слухайте, що каже команда і рефлексуйте їхні слова у шаблонах. Чим ближче шаблони будуть до вашої команди, тим скоріще прийде розуміння солід.

зробити з командою шаблони коду

А можно примеры этих магических шаблонов?
Не очень понятно зачем нада команда. Если есть шаблоны с которыми можно работать однозначно, то можно и автоматизировать процес создания кода.
СОЛИД — это принципы, а не шаблоны. Принципы выше по уровню абстракции. Тот же СРП мы с вами понимаем по разному.

Снова же не понятно как обяснить команде что методы:

public static hasUnlimitedContentAccess(user: User)
public static getBasicContent(movies: Movie[])

должны быть в одном классе?

для цього є ви, щоб допомогти писати краще :)

А какая у меня роль в проекте?

А можно примеры этих магических шаблонов?

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

Не очень понятно зачем нада команда. Если есть шаблоны с которыми можно работать однозначно, то можно и автоматизировать процес создания кода.

така автоматизація буде доступна через років так 50-60, а проект вам треба робити вже

СОЛИД — это принципы, а не шаблоны. Принципы выше по уровню абстракции. Тот же СРП мы с вами понимаем по разному.

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

Снова же не понятно как обяснить команде что методы:

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

А какая у меня роль в проекте?

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

Для анемичной модели это ± работает. Кстати, было бы интересно обсудить с вами вроде не самый сложный шаблон для репозитория. Особенно когда в него надо засунуть 5-10 методов для чтения.
А вот с DDD все уже сложнее.

Можемо наступного тижня спробувати. Зробимо Draw.io і пошарим доступ? Може є якісь кращі тули для колабораційного дизайну і коментів?

Анонс только запилите да

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

Интересный факт:
вы не ответили ни на один конкретный вопрос, но налили кучу воды и словоблюдия.

Щось це мені вже нагадує www.bruceblinn.com/parable.html за відношенням часу на мітинги та часу на розробку

За статтю дякую, але є декілька але:
1. Якщо я зарефакторю проект і буду там одночасно застосовувати всі ці підходи, то ревьювери зходять зрозуму коли мої коміти аффектять 50-100 файлів. Це змушує мене робити довгі ітерації і розбивати рефакторінг на маленькі коміти і чекати поки їх заапрувлять. Тому це камінь в бік код-ревью і тих ревюверів які не можуть осягнути сенс рефакторінгу. (Або питання, як в такому випадку проштовхнути рефакторінг не за пів року ?)
2. По опен-клоз принципу, тут основна мета — backward compatibility. Завжди користуюся цим принципом і по суті воно перекриває мету даного принципу
3. Задетектити фічу на перед і зробити оверінжинірінг із 10 інтерфейсів і 20 реалізацій — не завжди потрібно, тому тут треба всеж оцінювати ТЗ і/або його надавати у повному обсяці, а не видавати порціями після реалізації попередньої частини.
4. Обожнюю рефакторити і давати коду друге життя, замість безглуздого переписування з 0 з тими же проблемами, тому не бачу проблеми згодом рефакторити деякі частини коду, які дійсно цього потребують в процессі розвитку проекта. Це економічно ефективніше ніж наперед придумувати і обробляти кейси які ніколи не будуть існувати насправді.
5. Якось доводилося працювати з модулем в якому всі класи і методи були фіналізовані. Мабуть автори коду були фанатами теж деяких із цих принципів, тож забрали можливість використовувати поліморфізм і змусили мене не розширити модуль з кастомною реалізацією, а повністю написати свій модуль повторюючи функціонал існуючого на 90%. (Цей кейс вже не до вас, а просто на тему поста)

А можна буде в розширеній версії уточнити приклади порушення принципу Лісков? Якщо я перевизначаю метод і там не викликаю super() - це вже автоматом порушення? В Андроід так часто змінюють поведінку фреймворку для своїх потреб

Потому что нужна магическая таблетка «сделать зашибись». Как скрам.
А разбираться с архитектурой и подбирать под каждый конкретный случай химеру из нескольких паттернов — это сложно и долго.

жорсткість (Rigidity): кожна зміна викликає багато інших змін;
крихкість (Fragility): зміни в одній частині ламають роботу інших частин;

Самое смешное что при работе с Computer Vision на эту фигню вообще не обращаешь внимания, так как любое небольшое изменение методики или набора features часто влияет на не связанные с ней тесты.

Так что код приходится писать со слабой связностью через интерфейсы (так как иначе был бы вообще ад), но доступ к классам данных делать полностью открытый без какой-либо инкапсуляции (она не дает преимуществ и ухудшает производительность).

Главное в таком коде чтобы любое действие над определенным набором данных было автономным и независимым от остального кода. И да, процедурный подход здесь тоже неплохо работает, так как алгоритмы процедурны.

Ми інкапсулювали всі правила роботи з підписками в одному класі.

Ніт.
Вы открыли наружу информацию приватные поля класса (subscriptionType и subscriptionExpirationDate), тем самым нарушив инкапсулюцию и фактически сделав код процедурным.

Но все же наружение SRP в классе User есть, но решать это надо было разбиением юзера на 2 интерфейса Person и SubscriptionHolder.
А вот реализовывать их в одном классе или нескольких — это вопрос который требует понимание конкретной бизнес задачи.

Чому subscriptionType є приватним? Якщо я хочу дізнатись ім’я користувача, чи його імейл, то чим це відрізняється від того, що я хочу дізнатися тип підписки користувача?
Дані readonly, тобто модифікувати їх ззовні (та й всередині) не получиться. Звісно можна додати get set, але чи варто?
Повністю підтримую 2 інтерфейси і реалізацію цих інтерфейсів в одному класі чи декількох в залежності від конкретних бізнес задач.

Якщо я хочу дізнатись ім’я користувача, чи його імейл, то чим це відрізняється від того, що я хочу дізнатися тип підписки користувача?

Угу, ничем не отличается, все эти поля лучше держать приватными и не «узнавать их значения вне класса». Но это уже разговор про рич или анемик модель, а не про СРП.
Но даже при использовании анемик модели ваш пример — это антипаттерн, поскольку вы не выделили бизнес сценарии в сервисы, а просто вынесли куски кода в одно место.

При хорошо построеных сервисах у вас не будет, того что получилось с AccessManager:
— если методи статические, то вы не сможете их переопределить. Почему бы не вынести их в модель?
— 4 метода работают с одной сущностью (Movie) и 1 с другой (User) — вот это уже признак нарушения СРП.
— методы которые делают простые операции, вроде фильтрации коллекции. Сделай мы методы isBasic/isPremium у Movie мы можем просто избавится от этих методов. Тут кстати, интересный вопрос: а премиум пользователи не имеют права смотреть бейсик фильмы?

С использованием сервис-класов мы могли бы получить что-то такое:

class AccessChecker {
// вот тут можно начинать спори рич или анемик
  public boolean hasUnlimitedContentAccess(user: User) {
    const now = new Date();
 
    return user.subscriptionType === SubscriptionTypes.PREMIUM
      && user.subscriptionExpirationDate > now;
  }
}

class MovieReader {
  private final AccessChecker checker;
  private final MovieRepo repo;
 
// вот тут уже есть бизнес сценарий получения базового контента
  public Movie[] getBasicContent(user: User) {
//movies = repo.readSomeMovies();
//checker.hasUnlimitedContentAccess(user);
    return movies.filter(movie => movie.subscriptionType === SubscriptionTypes.BASIC);
  }

// вот тут уже есть бизнес сценарий получения премиального контента 
  public Movie[] getPremiumContent(user: User) {
//movies = repo.readSomeMovies();
//checker.hasUnlimitedContentAccess(user);
    return movies.filter(movie => movie.subscriptionType === SubscriptionTypes.PREMIUM);
  }
 
// следующие 2 метода непонятно зачем нужны, но пусть будут
  public Movie[] getContentForUserWithBasicAccess(user: User) {
//movies = repo.readSomeMovies();
//checker.hasUnlimitedContentAccess(user);
    return AccessManager.getBasicContent(movies);
  }
 
  public Movie[] getContentForUserWithInfiniteAccess(user: User) {
//movies = repo.readSomeMovies();
//checker.hasUnlimitedContentAccess(user);
    return movies;
  }
}

І получилось так, що

subscriptionType === SubscriptionTypes.PREMIUM

Зустрічається і в класі AccessChecker, і в класі MovieReader. Експертиза того, які є підписки і як з ними потрібно працювати вже є в обох класів. Але раз є в 2-ох, то може бути і в 3-ох. Наприклад, клас який відповідає за User Support. Там ж ми також десь напишемо subscriptionType === SubscriptionTypes.PREMIUM, бо у кого преміум, тому мають відповідати скоріше. Розсилка пошти — subscriptionType === SubscriptionTypes.PREMIUM і тепер лист в пурпурних тонах.
І все б нічого, до того моменту, коли додаємо ще одному підписку — Standart. Ми додали підписку, а чому це зачіпає усіх: AccessChecker, MovieReader, SupportService, EmailService. Ще раз, додалась ще одна підписка, а ми вже модифікуємо 4 класи. У нас експертиза по підписці розплилась по всій програмі. MovieReader — читає фільми і розуміє типи підписки. SupportService — опрацьовує запити користувачів і розуміє тип підписки. EmailService — відправляє лист і розуміє тип підписки. У нас тут вже у кожного по 2 responsibility.
Чому я у своєму прикладі зробив AccessManager і тільки у ньому тримав всі subscriptionType === SubscriptionTypes.PREMIUM. Тому що у ньому я можу повністю зусередити всю логіку по роботі з підписками. (AccessManager вже більше не здається мені вдалим іменем, думаю SubscriptionManager було б краще)
У ньому я можу написати методи аля getSupportPriority, getEmailBaseColor, getSomeOtherSubscriptionDependantValue. Це мій інформаційний експерт по підписках. Він скаже все, починаючи від того, як фільтрувати фільми, до того, в яких кольорах має бути лист. Я додаю Standant Subscription, і те, як фільтрувати тепер фільми я пишу тільки в AccessManager (SubscriptionManager), що зелений колір для листа я пишу тільки в AccessManager, що користувачі з стандартною підпискою мають більший пріоритет, ніж з базовою, я пишу в один єдиний AccessManager, бо це єдиний інформаційний екперт по тому, як поводитися з підписками.

если методи статические, то вы не сможете их переопределить.

В один випадках — це проблема. В інших навіть дуже добре, бо ми часто явно хочемо заборонити перевизначення.

— 4 метода работают с одной сущностью (Movie) и 1 с другой (User) — вот это уже признак нарушения СРП.

Єдина відповідальність != робота з єдиною сутністю. У сутності Фільм та Користувач є інформація про підписку. Наш клас відповідає за роботу з підписками. Цей клас ж не робить стрімінг відео, чи перевіряє логін і пароль користувача. Він сфокусований на тій частині сутностей, яка має інформацію про підписку.

subscriptionType === SubscriptionTypes.PREMIUM
Експертиза того, які є підписки і як з ними потрібно працювати вже є в обох класів. Але раз є в 2-ох, то може бути і в 3-ох.
Сделай мы методы isBasic/isPremium

Применимо и к юзеру. :) Вот та самая инкапсыляция в модели ,от который вы избавляетесь перенося все в хелперы, решает эту проблему.

В один випадках — це проблема. В інших навіть дуже добре, бо ми часто явно хочемо заборонити перевизначення.

Если у вас не возможны разные поведения для одной модели, почему бы не держать это поведение в самой модели?

Єдина відповідальність != робота з єдиною сутністю. У сутності Фільм та Користувач є інформація про підписку.

Угу, не равно, но разные сущности в одном сервисе — признак плохого дизайна. Если уж у вас в 2-х сущностях есть одинаковая информация (крякает как утка) и вы можете работать с ней методами которые находятся в одном класе (выглядит как утка), то имеет смысл вынести у этих сущностей общий интерфейс.

Еще важнык призанак того что код имеет плохой дизайн:
Наличие слов Сервис, Менеджер, Хелпер (или названий других паттернов) в названиях классов.

Применимо и к юзеру. :) Вот та самая инкапсыляция в модели ,от который вы избавляетесь перенося все в хелперы, решает эту проблему.

Я не впевнений, чи правильно вас зрозумів. Ви кажете, що хелпер — це SubscriptionManager і його треба перетворити в модель? Я ніде не писав, що SubscriptionManager — це хелпер. Це частина доменної моделі. Інформацій експерт з підписок. Якби ми робили розділення на DLL, то він би був разом з User і Movie в одній DLL Models. Тобто в одному пакеті. Так, у нього є статичні методи, але він сам не статичний. Його інстанс можна створити.

Если у вас не возможны разные поведения для одной модели, почему бы не держать это поведение в самой модели?

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

Еще важнык призанак того что код имеет плохой дизайн:
Наличие слов Сервис, Менеджер, Хелпер (или названий других паттернов) в названиях классов.

StringBuilder є в .NET і Java. Є такий GoF паттерн білдер. За вашою логікою .NET і Java мають ознаки поганого дизайду.
Я неодноразова бачив рекомендації і з ними дуже погоджуюсь, що додавання патерну до імені — дуже рекомендовано. Просто назва патерну вже вам скаже дуже багато про клас.

Я хочу, щоб subscriptionType був прозорим для всіх, крім менеджера підписок.

1) Зачем? «Хочу» — это не описание проблемы, которую нужно решить. Какую проблему ми решаем выделяя одно сравнение в утилитный метод?
2) Теперь пользователи завязаны на некий «менеджер подписок» и фильмы. Удачи в перенесении этих сущностей в разные модули/микросервисы.

StringBuilder є в .NET і Java. Є такий GoF паттерн білдер. За вашою логікою .NET і Java мають ознаки поганого дизайду.

В джаве 100500 примеров плохого дизайна и костылей. В дотНете уверен так же.

1) Зачем? «Хочу» — это не описание проблемы,

As a User I want to order a Product. Нічого не нагадує? ) Це мабуть чи не найраща форма опису юзер сторей.

StringBuilder є в .NET і Java. Є такий GoF паттерн білдер. За вашою логікою .NET і Java мають ознаки поганого дизайду.
В джаве 100500 примеров плохого дизайна и костылей. В дотНете уверен так же.

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

Еще важнык призанак того что код имеет плохой дизайн:
Наличие слов Сервис, Менеджер, Хелпер (или названий других паттернов) в названиях классов.

Це ваша суб’єктивна думка, а не щось загально прийняте в індустрії.

Вы меня просто убиваете :)

As a User I want to order a Product. Нічого не нагадує? ) Це мабуть чи не найраща форма опису юзер сторей.

Я понимаю что СА работает с НФР, но все же надо хоть немного быть знакомым с записью юзер сторей (en.wikipedia.org/...​er_story#Common_templates ).
Там есть очень важная секция которая как раз и поясняет мотивацию стори, то что я у вас спросил. Без секции с мотивацией, это пример наверное худшего описание юзер стори.

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

Нет, стандарты формируют всякие неучи, которые смотрят на примеры __учебного__ кода больших контор или коммюнити (типа спринга) и бездумно его копипастят. К слову, на ту же джаву часто наезжают за дурацкие названия типа ЧтоТоТамБилдерФекториСервис.

Це ваша суб’єктивна думка, а не щось загально прийняте в індустрії.

Зачем вы это написали? Поняли что аргументация которую вы привели выше слабовата, но ничего лучше не придумали, чем пытаться указать что мнение вашего опонента менее авторитетно чем то чем вы прикрываетесь?

Поясняю проблему с паттернами в именах классов:
Объекты болжни отображать сущности реального мира, соответственно название их классов так же должны соответствовать названиям из реального мира. Паттерны — это виртуальные сущности (особенно такие неосмысленные как Менеджер?Хелпер/Утилс). Имена паттернов в названии класса — это признак того что автор класса не (полностью) понимает какую сущность отображает его класс.

Объекты болжни отображать сущности реального мира, соответственно название их классов так же должны соответствовать названиям из реального мира.

Фигня. Объекты есть там, где они упрощают программирование. Пример: сокет не является объектом реального мира. Пример: поток не является объектом реального мира. Пример: юз кейс не является объектом реального мира.

Прокси — имя паттерна. Будете жить безх прокси, так как они не являются объектами реального мира? Или будете не называть прокси «Прокси» чтобы не повторить имя паттерна в названии?

Пример: сокет не является объектом реального мира.

Сокет — это объект мира который мы моделируем, точка потключения.

Пример: поток не является объектом реального мира.

Поток — так же отображает реально существующий поток пыполнения команд.

Пример: юз кейс не является объектом реального мира.

Юз кейс — это пользовательский сценарий, вполне себе объект для какой-то системы управления, этими самимы сценариями.

Прокси — имя паттерна. Будете жить безх прокси, так как они не являются объектами реального мира? Или будете не называть прокси «Прокси» чтобы не повторить имя паттерна в названии?

Вы сейчас говорите о «прокси сервере» или о паттерне?
В случае с сервером, нормальное название, потому что отображает объект реального мира.
В случае с паттерном, плохое название так как оно не отображает того для чего создан этот прокси, какую функциональность добавляет этот прокси (например кеширование или логирование).

Эти все объекты виртуального мира в голове, а не реального. Еще скажите, что Медиатор и Фабрика (или Билдер)- объекты реального мира. Или что их не нужно использовать.

Прокси — паттерн. Он отображает объект реального мира?

Эти все объекты виртуального мира в голове, а не реального.

Ок, меняем словосочитание «реальный мир» на «предметную область».

Еще скажите, что Медиатор и Фабрика (или Билдер)- объекты реального мира. Или что их не нужно использовать.

Ээээ? Я как раз сказал обратное :)

Прокси — паттерн. Он отображает объект реального мира?
В случае с паттерном, плохое название так как оно не отображает того для чего создан этот прокси, какую функциональность добавляет этот прокси (например кеширование или логирование).
Объекты болжни отображать сущности реального мира, соответственно название их классов так же должны соответствовать названиям из реального мира. Паттерны — это виртуальные сущности (особенно такие неосмысленные как Менеджер?Хелпер/Утилс). Имена паттернов в названии класса — это признак того что автор класса не (полностью) понимает какую сущность отображает его класс.

Вы для паттернов не создаете классы?
Или классическая XWindowSystemFactory из GoF непонятно названа?
Или такой класс не нужен при поддержке нескольких оконных систем?
Или SaveCommand можно как-то лучше обозвать, чтобы в названии не было Command?
Ну и напишите, как Вы назовете класс, проксирующий удаленную сущность (базу данных или другой класс).

Или классическая XWindowSystemFactory из GoF непонятно названа?

XWindowSystems

Или SaveCommand можно как-то лучше обозвать, чтобы в названии не было Command?

Save?

Ну и напишите, как Вы назовете класс, проксирующий удаленную сущность (базу данных или другой класс).

Remoteудаленную сущность

Всегда есть трейдоф между моделированием предметной области и скоростью разработки. Иногда проще добавлять название шаблона и не думать.
Интересный момент в том что вы привели примеры из области «системных задач», ввиду абстрактности предметной области, XWindowSystemFactory и SaveCommand там могут быть частью предметной области (тот же СтрингБилдер так же является частью предметной области, хотя и по своей сути является затичкой).
Эту ветку ми начали с примера, который моделирует бизнес процес, там довольно редко паттерны вплетены в предметную область.

По моему мнению, паттерны — это хаки, когда DDD нормально не работает, и за углом слышится дыхание северного зверька. Поэтому обычно они не будут мапиться на предметную область, а создаются исключительно для гибкости и удобства программирования.

По моему мнению, паттерны — это хаки, когда DDD нормально не работает,

Какбэда.

Клас User в першому прикладі порефакторився погано. В нього як було дві задачі, так і залишилось. От що ми будемо в цьому прикладі робити, якщо нам доведеться, наприклад, додати кількість девайсів та користувачів у підписці? Додавати їх до классу юзера, там же вже є два поля, що описують тип підписки і дату її закінчення! І тепер ми бачимо, що наш клас описує не юзера, а «юзера_та_підписку», що якраз і є порушенням Single Responsibility.
А треба було виділити все, що стосується підписки, в окремий клас. Тоді і розширювати його буде простіше, і юзер залишиться юзером, і метод hasUnlimitedContentAccess ніхто вже в клас User не покладе (адже є клас підписки).

Це я до того, що спочатку потрібно рефакторити дані, а методи вже потім самі-собою попереносяться.

Нехай у користувача є email — user.email. Що ми будемо робити, якщо додасться ще alternative email та isValidatedEmail? Ну нехай ми створимо об’єкт UserEmails і будемо в ньому зберігати посилання на користувача і всі його імейли, включаючи основний, і чи ці імейли провалідовані. А що, якщо така ж сама ситуація буде і з адресою проживання? А з прізвищем? А з імнем? Якщо слідувати за логікою все «окремий об’єкт», то у нас буде об’єкт User, в якого є тільки ID. Чи буде у нас користь з такого об’єкта? А якщо такі об’єкти ще й будуть зберігатись в реляційній базі даних, скільки це join треба зробити, щоб зібрати всю інформацію про об’ект.
Якщо підписка користуча визначає кожен його крок (які фільми можна дивитися, який інтерфейс показувати, які фічі активувати) і в осяжному майбутньому вона не буде змінюватися, то навіщо нам робити окремий об’єкт UserSubscription, якого весь час його доведеться шукати по user.id? Якщо тип підписки є такою ж невід’ємною частиною сутності користувача, як і email, name і address, то навіщо її відділяти?
Відповідаючи на ваше запитання про кількість дивайсів і користувачів у підписці. Давайте розглянемо UI/UX. Ми десь цю інформацію збираємось показувати? Як це вплине на функціонал користувача? Скоріш за все ця інформація буде використовуватися під час авторизації запиту на перегляд. Якщо кількість дивайсів перевищила максимально можливу, або з вашого акаунту дивляться більше, ніж дозволено користувачів, то вам верне 403 і ви покажете коирстувачу помилку. І тут у вас не буде великий об’єкт User, в якому буде все. У ньому буде лише тип підписки. За типом підписки ви отримаєте деталі підписки (іншим запитом у базу або кеш), а саме кількість дивайсів та користувачів.
Треба в першу чергу розуміти свій домен. Користувач, який дивиться фільми, не такий самий користуч, який грає в одлайн ігри. Якщо невід’ємна частина першого є його підписка на контент, то невід’ємною частиною другого є те, чи він онлайн.

Нехай у користувача є email — user.email. Що ми будемо робити, якщо додасться ще alternative email та isValidatedEmail? Ну нехай ми створимо об’єкт UserEmails і будемо в ньому зберігати посилання на користувача і всі його імейли, включаючи основний, і чи ці імейли провалідовані. А що, якщо така ж сама ситуація буде і з адресою проживання? А з прізвищем? А з імнем?

Как вариант. Даже больше, e-mail неплохой кандидат на отдельный класс.
А что если у пользователя несколько подписок одновременно? Без сценариев использования невозможно однозначно сказать соблюдаются SOLID принципы или нет, и мы с вами будем просто играть в угадайку.

email з додатковими даними про дату створення і чи він провалідований дуже добре підпадає під Value Object в DDD.

Без сценариев использования невозможно однозначно сказать соблюдаются SOLID принципы или нет

Погоджуюсь. Контекст зажди має дуже велике значення. Під контекстом я маю на увазі сценарії використання і бізнес домен.

Оскільки з файлу ми читаємо локально, то метод Connect зайвий.

Ну как-бы соединение с базой и открытие файла семантически схожи. И ошибка коннекта или открытия файла это одно, а ошибка чтения или парсинга данных — другое. Так что спорно, что здесь нужно уходить от общего интерфейса.

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

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

Так у вас же работа не с файлами, а с «источниками данных». То что источником является локальный файл — это деталь реализации.
Дальше интереснее:
разделив ремоут и не ремоут датасорсы вы рискуете получить код который работает с датасорсом и не имеет возможности использовать ремоут датасорсы, потому что банально некуда засунуть конект к датасорсу.
И мы все понимает что закончится это ифом с инстансофом :)

Connect передбачає підключення до чогось. Підключення до локального файлу як мінімум звучить дивно.
Так у вас же работа не с файлами, а с «источниками данных». То что источником является локальный файл — это деталь реализации.

Так все ж так і є. IDataSource та IRemoteDataSource — це абстракції. IRemoteDataSource додаткового вимагає ще метода сonnect. До файлів, локального кеша, та навід вводу з command line вам підключатись не потрібно. Тому їх реалізацій ви будете наслідувати IDataSource. До віддалених джерел даних, таких як бази даних, розподілені кеші, розподілені черги ви маєте якось спочатку підключитись. Тому до їх реалізацій ви будете використовувати IRemoteDataSource, який вимагає методу connect.

разделив ремоут и не ремоут датасорсы вы рискуете получить код который работает с датасорсом и не имеет возможности использовать ремоут датасорсы

Ну, що краще, робити додаткову перевірку на IRemoteDataSource і викликати connect тільки для тих об’єктів, які дійсно його потребують, чи викликати зайвий метод connect, щоб прочитати файл, локальний кеш чи командну стрічку?

До файлів, локального кеша ... розподілені кеші,

Вот это то о чем я говорил: работаете вы с локальным кешом, появляется необходимость работать с удаленным и ... вы переписываете ваш код на ноые абстракции :)

Ну, що краще, робити додаткову перевірку на IRemoteDataSource і викликати connect тільки для тих об’єктів, які дійсно його потребують, чи викликати зайвий метод connect, щоб прочитати файл, локальний кеш чи командну стрічку?

Лучше пользоваться абстракциями. Неужели Solution Architect-у нада разъяснять зачем нам абстракции и почему инстансоф это признак плохого дизайна?

P.S.

командну стрічку

Что такое «командная лента»?

Вот это то о чем я говорил: работаете вы с локальным кешом, появляется необходимость работать с удаленным и ... вы переписываете ваш код на ноые абстракции :)

Для розподіленого кеша буде інший метод, інший клас та інтерфейс буде іншим. Я сумніваюсь, що в один момент у мене є клас, чи метод для роботи з локаним кешом (кеш в memory space процесу) і я в один момент його переписую, щоб він здіснював віддалений доступ до іншого процесу, або сервера і брав звідти дані. Є fine grained та coarse grained інтерфейси. fine grained ви використовуєте, коли доступаєтесь до адресного простору вашого процесу. Наприклад, ви можете окремо змінити адресу користувача, та окремо його ім’я, тому що доступ до пам’яті процесу швидкий. Якщо ви здійснюєте віддалений доступ, ви витрачаєте додатковий час на міжпроцесорну комунікацію або мережеву комунікацію. Вам вже дорого окремо змінювати адресу користувача і його ім’я, тому ви будете робити це за раз одним викликом. Це coarse grained.

почему инстансоф это признак плохого дизайна

instanceof — це відносно перевірки IRemoteDataSource? Вам instanceof не обов’язково використовувати. Краще зробити так f(remoteDataSource: IRemoteDataSource).

Что такое «командная лента»?

Command Line, Terminal, cmd, cin, cout, Console.Read, Console.Write

Лучше пользоваться абстракциями.

Ніхто про це не сперичається. Я про це пишу в розділі Dependency Inversion цієї статті.

Є fine grained та coarse grained інтерфейси. fine grained ви використовуєте, коли доступаєтесь до адресного простору вашого процесу. Наприклад, ви можете окремо змінити адресу користувача, та окремо його ім’я, тому що доступ до пам’яті процесу швидкий. Якщо ви здійснюєте віддалений доступ, ви витрачаєте додатковий час на міжпроцесорну комунікацію або мережеву комунікацію. Вам вже дорого окремо змінювати адресу користувача і його ім’я, тому ви будете робити це за раз одним викликом. Це coarse grained.

Нафіг? Чому не зробить поліморфізм, коли маєте інтерфейс з методами Begin(), ChangeX(), Commit(). Для локальної взаємодії Begin() та Commit() порожні, а сеттери працюють напряму. Для віддаленої — усі зміни збираються в пакет, і комміт цей пакет відсилає.

Клас з 3-ма методами. 1 щось робить. 2 пусті. Просто, щоб попадати під один інтерфейс. Можливо, але для цього мають бути серйозні аргументи, щоб на це всі погодились.
Ви згадали про сетери, але нічого не сказали, про гетери. Якщо мій інтерфейс каже get(): Object, я безпечно можу його використати в циклі з 1000000 кроків. Якщо мій інтерфейс мені каже get(): Promise < Object >, то я вже подумаю, чи можу його просто так в циклі використовувати. Знову батчітг. Добре, але дістати 1000000 елементів — не дуже дешево і може їх доведеться десь локально кешувати. Або тримати всі 1000000 під одним ключем. Таке рішення породжує дуже багато питань. Я все ж схиляюсь до думки, що треба розділяти fine grained та coarse grained

Клас з 3-ма методами. 1 щось робить. 2 пусті

Якщо Ви робите доступ до одного поля класу, то він не може буть coarse grained. Якщо coarse grained — то матимете цілу купу сеттерів для різних полів та колекцій.

Якщо мій інтерфейс каже get(): Object, я безпечно можу його використати в циклі з 1000000 кроків. Якщо мій інтерфейс мені каже get(): Promise < Object >, то я вже подумаю, чи можу його просто так в циклі використовувати.

І що Ви зробите тоді? А можна на колекції мати віртуальні ForAll() чи Begin() та Commit() і писати однаковий код для локальних та віддалених даних. Звичайно, якщо у Вас не реал-тайм. Але в ньому й з промісами не попишете.

Не проблема, як це зробити на колекції. Нехай у вас запит до віддаленого кешу 1 мілісекунда. Прохід по колекції, яка робить енумерейшен 1000 елементів віддаленого кеша займе 1 секунду. Це так трохи довго. Для бази данних можна зробити варіант з Deffered execution docs.microsoft.com/...​#deferred-query-execution. Тоді ви не витягуєте дані з бази рядок за рядком, а ваша колекція робить одну квері і всі обчислення зробить база даних. Але навантажувати базу також не варто. І таке не спрацює для Редіса.
Мій поінт в тому, що якщо у мене метод кеша повертає об’єкт, то я його можу безпечно використовувати скільки завгодно разів. Якщо у мене метод повертає проміс і мені цей метод треба використовувати n разів, то я буду по можливості такий метод уникати, бо це може принести вже імпакт на 10-100 елементах.

А я кажу, що Ваш пойнт стосовно трьох методів в класі трохи дивний, бо якщо в класі один аксесор, то Ви не можете до такого класу зробить coarse grained доступ, бо там завжди один елемент даних. А коли в класі багато аксесорів, то методи Begin() та Commit() мають сенс, щоб не розводить різні куски коду на одну операцію.

І не треба повертати проміс, і намагатись працювати з промісами по-одному. Треба поюзать Combined/Batch/Enumeration Method (foreach) чи проксю з Begin() та Commit(). Матимете однаковий код для локального та віддаленого доступу, і навіть локально воно може почати швидше працювати. І логіка роботи з даними не буде розмазаною по овердофіга методів, бо вона має поміститись між викликами Begin() та Commit() чи всередині аргумента до foreach().

Підключення до локального файлу як мінімум звучить дивно

Це звучить дивно для людини, яка ніколи не бачила файлову систему *nix. Там все що завгодно може бути файлом: конекшин до бази, сокета, принтера, процесу, девайса, тощо.

Стоит сказать, что проблемы с которыми борются Open/Close, Liskov Substitution и даже в определённых случаях SRP, порождены во многом ограничениями системы типов некоторых ООП языков, которые навязывают полиморфизм через наследование. И как альтернативу предлагают только примитивный subtyping на интерфейсах, но при этом не дают это делать ретроактивно (в расширении, например), а заставляют указывать родство типов прямо в декларации, как у автора в примере.

При наличии статической расширяемости типов, параметрического полиморфизма или ad-hoc полиморфизма с ранним связыванием; алгебраических типов данных и функций высшего порядка — едва ли кому-то придёт в голову применять наследование или модифицировать реализацию кроме ситуаций, когда это действительно необходимо.

Иными словами, иногда более правильным решением будет смена языка программирования.

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

Коли біг по статті очима і побачив слово static, в мене вилетів Exception, і я далі не читав

Попробуй обновить парсер, думаю, эту багу уже пофиксили.

І де в ООП є static? Kotlin? Go? Нові мови такого костиля не мають

Листинги в статье на JS, парсер заточенный на Kotlin или Go может бросать исключения, да.

Цікаве твердження про нові мови програмування. Я з Котліном не мав справи, але знайшов, що там є companion object та @JvmStatic. У всіх посиланнях я знаходжу це як аналог до Java static. Але ніхто не каже, в чому принципова різниця. Ну так, ви зайжди викликаєте метод з companion об’єкта, а не класу. Але це суті не змінює, виклик залишається MyClass.f(). Хотілось би розібратись в цьому питання та почути вашу думку.
Також, чи ви вважаєте Rust та Swift новими мовами програмування? На скільки я знайшов, у них є статичні методи.
А в чому проблема таких методів як Console.Read() та JSON.parse? Невже new Console().Read() дає якісь переваги?
Також не експерт в Gо, але на скільки я прочитав, то кажуть що в Go є структури, а не класи і цим пояснюють відсутність static.
У мене є певний досвід проблем з статичними методами, особливо покриття їх тестами. Але я не можу сказати, що це катастофічні проблеми. Тому дуже хотілось би почути аргументовану розгорнуту відповідь.

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

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

Строго говоря, instance методы — это те же статические методы, но с неявным self параметром.
Другое дело, что языки с примитивной системой типов не дают никаких вариантов полиморфизма для статических методов. У них нет таблиц вызовов (поэтому и static) и их нельзя использовать как функции высшего порядка.

В итоге, в таких языках статические методы, по-сути, становятся аналогом процедурного in-line программирования, без возможности полиморфизма. В довесок, поскольку все объекты — это ссылки, то ещё и получаем проблему мутации глобального состояния, null reference, вот это всё.

В языках типа Swift, static методы не являются проблемой совершенно, потому что язык поддерживает функции высшего порядка: можно mock-ать статический метод подменяя его замыканием (лямбдой) или любой другой функцией того же типа. Также язык позволяет объявлять static методы в протоколах (аналог Java интерфейса) и реализовывать их в sub-типах, что даёт табличный полиморфизм. Также язык поддерживает истинную иммутабельность и null safety, что в значительной мере защищает нежелательный write-доступ к глобальному состоянию.

В итоге, static методы в Swift идиоматически становятся подвидом «свободных» функций, но заключённых в namespace типа.

Единственное, чего нет ни в Swift, ни тем более в классических ООП языках — это формального механизма объявления чистых функций/методов. Поэтому, при желании, мутировать внешнее состояние и даже поймать исключение всё равно можно.

Спасибі. Дуже вичерпна відповідь. Ще раз дякую!

Извиняюсь, что докапался, но название метода hasInfiniteContentAccess обманчивое. Оно подразумевает, что доступ бесконечный, хотя мы явно проверяем дату истечения подписки. Наверное, имелся в виду unlimited access.

Имелся в виду доступ к неограниченному количеству контента, а не неограниченный по времени доступ к контенту. Да, название кривое, hasUnlimitedContentAccess it is 🙂.

Дуже слушне зауваження. Вже виправили. Дякую!

Принцип підстановки Барбари Лісков є найбільш «підступним» пунктом серед усіх п’яти, а уваги йому приділено менше за інших.

SOLID, к сожалению, часто лишь средство дешёво и просто указать кому-то, что его код гавно, не имея каких-то адекватных доказательств.
Всегда может оказаться, что Ваш класс делает 2 responsibility в мозгу собеседника, так как формального определения нет, есть лишь привкус.
Если бы формально было бы возможно описать SOLID, уже были бы формальные StyleCop, непозволяющие коммитить не-SOLID код, но что-то я про такое не слышал.

SOLID — это набор адекватных рекомендаций, следуя которым уменьшается риск написать неподдерживаемый говнокод. Насколько ему следовать зависит от того как принято на проекте/в команде — к примеру, врядли есть смысл сильно увлекаться следованием приципам на поддержке старого легаси-проекта который уже много лет держится исключительно на костылях (если только задача не заключается именно в рефакторинге). А если вам попадались ревьюверы которые просто искали к чему придраться, вам не повезло, но проблема тут явно не в SOLID.

Коли біг очима по статті, то аж відчув ніби сходив на інтервю, але так десь в 2011-2013 році :)

Давно не ходили на інтерв’ю? Чи перейшли на js?

Ні, на js не перейшов) А як це може бути звязано? :)

Це був невдалий жарт 😅 Не буду продовжувати :)

Из серии на собесе SOLID, а открываешь код проекта а там нет SOLID-а.

История из жизни: менеджер (из бывших девелоперов) рассказывает команде как правильно писать код — SOLID, TDD, покрытие тестами, Sonar checks. потом открываешь какой-то старый код , напичканный разными антипаттернами, юнит тесты отсутствуют — и там в истории его имя в коммитах в качестве девелопера

Так он же все время учился, и доучился до ПМ.

эволюция, самые умные уже не пишут код

кто умеет тот делает кто не умеет делать тот учит других как надо делать кто не умеет ни того ни другого тот руководит

Ну мне как бы тоже трудно без слёз смотреть на собственный код, написанный лет 20 назад.

Підписатись на коментарі