image


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

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

В статье не будет упора на конкретный язык программирования, ниже написанное актуально для всех процедурных языков. Но, как мне кажется, статья будет особенно полезна тем, кто программирует на динамических интерпретируемых языках, и не имел серьезного опыта разработки на типизированных компилируемых языках. Именно в коде данной категории разработчиков я чаще всего замечал описанные ниже паттерны. Примеры будут на псевдокоде, схожем с Java, C# и TypeScript.

Сразу оговорюсь, что в статье не будет рассмотрен вопрос о том, как нужно писать тесты. На эту тему и так существует немало статей. Будет рассмотрен вопрос, как нужно писать код, чтобы его можно было просто и красиво тестировать, а получившиеся тесты получались простыми и поддерживаемыми. Далее под понятием тест подразумевается красивый и чистый unit тест, который написан без различных хаков, без дополнительных «магических» библиотек для подмены зависимостей на лету, и прочего удовольствия, затрудняющего чтение и поддержку тестов. То есть тест в самом прекрасном значении этого слова.

Немного философии. Стоит ли вообще писать тесты? Мое мнение: если вы создаете проект, который будет развиваться, то вам просто необходимы тесты. Помимо того, что тесты выполняют свою прямую функцию (позволяют проверять соответствие кода требованиям), они как побочный эффект «выпрямляют» дизайн классов. Все потому, что на класс с «кривым» дизайном тесты не напишешь, следовательно, чтобы добиться тестируемость приходится рефакторить класс. Либо, если тесты пишутся до кода, дизайн класса сразу рождается правильным. Тестируемый код является переиспользуемым, так как мы можем повторно использовать его в тестах. А переиспользуемость — важный критерий правильного дизайна класса. Так же тесты, возможно, но совсем не обязательно, улучшат архитектуру приложения в целом. Как кто-то хорошо подметил: дизайн класса хорош ровно настолько, насколько данный класс можно протестировать с помощью unit тестов.

Список главных убийц тестируемого кода:




Добыча знаний


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

Метод или конструктор должен требовать то, что ему реально нужно для работы. Он не должен сам заботиться о том, как все это получить/вычислить, выполняя какую либо работу. Нужно требовать минимально необходимый набор аргументов, для своей работы.

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

до: не тестируемый код
class DiscountCard {
	DiscountCard(UserContext userContext) {
		this.user = userContext.getUser();
		this.level = userContext.getLevel();
		this.order = userContext.getOrder();		
	}
	
	// ...
}

// тесты

UserContext userContext = new UserContext();
userContext.setUser(new User("Ivan"));
PlanLevel level = new PlanLevel(143, "yearly");
userContext.setLevel(level);
Order order = new Order("SuperDeluxe", 100, true);
userContext.setOrder(order);
DiscountCard discountCard = new DiscountCard(userContext);

// можно тестировать


DiscountCard не использует напрямую userContext. Тому, кто будет писать тест, нужно будет изучить устройство класса DiscountCard, чтобы понять какие объекты там реально требуются, ведь userContext может содержать десятки объектов для инициализации. А это время и риск, что то сделать не так, а в результате тест может оказаться неправильным. Сделаем так, чтобы DiscountCard требовал то, что ему действительно нужно:

после: тестируемый код
class DiscountCard {
	DiscountCard(User user, PlanLevel level, Order order) {
		this.user = user;
		this.level = level;
		this.order = order;		
	}
	
	// ...
}

// тесты


User user = new User("Ivan");
PlanLevel level = new PlanLevel(143, "yearly");
Order order = new Order("SuperDeluxe", 100, true);

DiscountCard discountCard = new DiscountCard(user, level, order);

// можно тестировать


Также этот пример намеренно иллюстрирует знаменитый паттерн ServiceLocator, который хранит в себе все зависимости приложения. Именно поэтому такой паттерн считается анти паттерном. Старайтесь избегать его.

Рассмотрим еще один пример добычи знаний:

до: не тестируемый код
class SalesTaxCalculator {
	TaxTable taxTable;
	SalesTaxCalculator(TaxTable taxTable) {
		this.taxTable = taxTable;
	}
	float computeSalesTax(User user, Invoice invoice) {
		// аргумент "user" не используется напрямую
		Address address = user.getAddress();
		float amount = invoice.getSubTotal();
		return amount * taxTable.getTaxRate(address);
	}
}

// тесты

SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable());

Address address = new Address("Ленина 23 ...");
// много кода чтобы создать "user"
User user = new User(address, ...);
Invoice invoice = new Invoice(1, new ProductX(95.00));
assertEquals(calc.computeSalesTax(user, invoice), 100);


В тесте необходимо создать класс User, хотя от него требуется только адрес. То же самое можно сказать про класс Invoice. Опять же, от того, кто пишет тест, требуется «прошерстить» код SalesTaxCalculator, чтобы разобраться, что же там реально нужно. Багоёмкое место.

Также, SalesTaxCalculator невозможно переиспользовать в другом проекте, в котором нет классов User и Invoice.

после: тестируемый код
class SalesTaxCalculator {
	TaxTable taxTable;
	SalesTaxCalculator(TaxTable taxTable) {
		this.taxTable = taxTable;
	}
	
	float computeSalesTax(Address address, float amount) {
		return amount * taxTable.getTaxRate(address);
	}
}

// тесты 

SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable());
Address address = new Address("Ленина 23 ...");

assertEquals(calc.computeSalesTax(address, 95.00), 100);


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

Оператор new в бизнес коде


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

до: не тестируемый код
class House {
	Kitchen kitchen; 
	Bedroom bedroom = new Bedroom();
	
	House() {
		this.kitchen = new Kitchen(new Refrigerator());
	}
	
	// ...
}

// тесты

House house = new House();
// ээээм, непонятно как тестировать
// нет доступа к kitchen и bedroom



В этом коде плохо все. Его невозможно протестировать, потому что вызов любого метода House приведет к вызову kitchen и/или bedroom, а их мы не контролируем. Если они общаются с БД или шлют запросы в сеть, то тесты обречены. При помощи полиморфизма невозможно подменить кухню или спальню, наш дом жестко завязан на определенные классы. Как сделать код тестируемым?

после: тестируемый код
class House {
	Kitchen kitchen; 
	Bedroom bedroom;
	
	House(Kitchen kitchen, Bedroom bedroom) {
		this.kitchen = kitchen;
		this.bedroom = bedroom;
	}
	
	// ...
}

// тесты
Kitchen kitchen = new DummyKitchen(null); 
Bedroom bedroom = new DummyBedroom();
House house = new House (kitchen, bedroom);

// Замечательно, легковесные моки под моим контролем


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

Но некоторые читатели скажут: «Минуточку. Если теперь все зависимости надо требовать в конструктор, то для того чтобы создать „глубокий“ дочерний класс (класс, находящийся в глубине графа зависимостей приложения), мне придется прокидывать все зависимости такого класса через родительские классы. А это приведет к тому, что конструкторы „верхних“ классов (классы, находящиеся ближе к началу графа зависимостей) превратятся в скопище всего и вся. А относительно примера это значит, что тому классу, который делает new House теперь придется самому требовать в конструктор kitchen, и bedroom. Но почему он должен про них знать? Это ж бред.»

Только в этих рассуждениях есть одна ошибка. Следуя выше обозначенному принципу, класс, создающий House, вообще НЕ должен его создавать. Ведь, согласно принципу, он сам должен требовать House себе в зависимости. Но тогда откуда же, в конце концов, возьмется класс House? Он будет создан на старте приложения (если время жизни House совпадает с временем жизни приложения) либо его создаст фабрика (если время жизни House меньше времени жизни приложения):

создание класса House
class HouseFactory {
	House build() {
		Kitchen kitchen = new Kitchen(new Refrigerator()); 
		Bedroom bedroom = new Bedroom();
		return new House (kitchen, bedroom);
	}
}


Но некоторые опять возразят: «Так что теперь, создавать фабрику для каждого класса, увеличивая объем кода в 2 раза? Это ж бред». На самом деле одна фабрика создает и связывает классы с одинаковым временем жизни. А в реальных приложениях не такое большое количество кода с различным временем жизни. Для примера рассмотрим, какие типы объектов, разбитых по различному времени жизни, существуют в веб приложении:
  • долго живущие объекты (создаваемые на старте приложения)
  • объекты сессии
  • объекты запроса
  • редко существуют объекты с временем жизни меньше запроса

Получается, хватит четырех фабрик для типичного веб приложения. Поэтому не стоит их бояться (а ниже в заключение будет показано, как фабрики можно вообще не писать).
Таким образом, можно выделить две группы кода:
  • группа бизнес кода
  • группа связующего кода

Бизнес код — код, отражающий бизнес логику, код, который будет меняться, когда изменятся требования заказчика. Это главная категория кода, собственно ради чего мы и пишем код.

Связующий код — код, который позволяет бизнес коду взаимодействовать, связывает его между собой. Связующий код не содержит в себе никакой бизнес логики. Сюда относятся фабрики, стартовая точка приложения и прочие связующие места. В связующем коде много оператора new.

Такой подход с разделением кода на 2 группы позволяет избежать смешивания логики приложения с созданием объектного графа приложения. Поэтому всегда при использовании оператора new задумывайтесь, в той ли группе кода вы его применяете.

Исключение:

оператор new можно использовать в бизнес коде для создания объектов-хранилищ, не содержащих поведения (например, HashMap, Array).

Рассмотрим еще один распространенный случай на следующем примере:

до: не тестируемый код
class DocumentActions {
	Network network;
	DocumentModel documentModel;
	
	DocumentActions(Network network, DocumentModel documentModel) {
		// не используется напрямую
		this.network = network;
		this.documentModel = documentModel;
	}
	changeTextStyle(int textOffset, TextStyle style) {
		Revision revision = new Revision(this.network, this.documentModel, textOffset);
		revision.updateTextStyle(style);
		revision.apply();
	}
	insertParagraph(int textOffset, ParagraphProps props, ParagraphStyle style) {
		Revision revision = new Revision(this.network, this.documentModel, textOffset);
		revision.addParagraph(props);
		revision.updateParagraphStyle(style);
		revision.apply();
	}
	
	// много таких же методов с new Revision
}

// тесты

// кто знает как трудно создать этот класс
Network network = new Network(...); 
DocumentModel documentModel = new DocumentModel(...);

DocumentActions docActions = new DocumentActions(network, documentModel);

// не понятно как проверить методы docActions


Класс DocumentActions в каждом своем методе создает класс Revision, лишая тесты возможности подменить реализацию этого класса на моки. А что еще хуже DocumentActions требует в конструктор классы, которые не использует. Но что делать, если для создания Revision каждый раз нужно отдавать третий аргумент textOffset, который заранее не известен? Выход: создать класс, который возьмет на себя знание, о том, как создавать Revision. А это не что иное, как фабрика:

после: тестируемый код
class DocumentActions {
	RevisionFactory revisionFactory;
	
	DocumentActions(RevisionFactory revisionFactory) {
		this.revisionFactory = revisionFactory;
	}
	changeTextStyle(int textOffset, TextStyle style) {
		Revision revision = this.revisionFactory.build(textOffset);
		revision.updateTextStyle(style);
		revision.apply();
	}
	insertParagraph(int textOffset, ParagraphProps props, ParagraphStyle style) {
		Revision revision = this.revisionFactory.build(textOffset);
		revision.addParagraph(props);
		revision.updateParagraphStyle(style);
		revision.apply();
	}
	
	// много таких же методов с this.revisionFactory.build
}

class RevisionFactory {
	Network network;
	DocumentModel documentModel;
	
	RevisionFactory(Network network, DocumentModel documentModel) {
		this.network = network;
		this.documentModel = documentModel;
	}
	Revision build(int textOffset) {
		return new Revision(this.network, this.documentModel, textOffset); 
	}
}

// тесты

class MyMockRevision extends Revision {
	// мокаем нужные методы
}

class MyMockRevisionFactory extends RevisionFactory {
	public Revision revision;
	Revision build(int textOffset) {
		this.revision = new MyMockRevision(this.network, this.documentModel, textOffset); 
		return this.revision;
	}
}

RevisionFactory revisionFactory = new MyMockRevisionFactory(null, null);
DocumentActions docActions = new DocumentActions(revisionFactory);

// можно тестировать 
// есть доступ к Revision через revisionFactory.revision


Фабрика знает, что для создания Revision точно нужны Network и DocumentModel. Поэтому она сама требует эти классы для себя. А все динамические параметры (textOffset), для создания Revision, будут требоваться в качестве аргументов метода build фабрики.

Теперь, если в будущем класс Revision потребует еще один постоянный аргумент в конструктор, то не составит труда немного поправить RevisionFactory, а класс DocumentActions останется вообще без изменений.

Глобальные переменные и синглтоны


Думаю, все согласятся с тем, что глобальные переменные это зло. Но многие не видят ничего плохого в синглтонах, хотя по своей сути это все те же глобальные переменные. Так чем плохи синглтоны? Вот 2 причины (хотя достаточно любой из них):

1) Проблема в использовании синглтонов в том, что они вносят жесткую связность в код. Создавая синглтон, вы говорите, что ваши классы могут работать только с одной единственной реализацией, которую обеспечивает синглтон. Вы не закладываете возможности заменить его. Это делает трудным тестирование класса в отрыве от синглтона, ведь сама природа теста требует возможности заменять реализации на альтернативные. Пока вы не измените такой дизайн, вы вынуждаете себя полагаться на правильную работу синглтона, чтобы тестировать любой из его клиентов.

2) Наличие синглтонов заставляет ваши классы врать о своих зависимостях, так как вносит «невидимые» зависимости. Чтобы понять реальные зависимости класса, вам нужно полностью читать его код, вместо того, чтобы просто взглянуть на список зависимостей конструктора/метода. И рано или поздно это приведет к тому, что тесты начнут влиять друг на друга, через скрытое глобальное состояние.

Пример из практики, иллюстрирующий сразу обе проблемы: в web приложении возникла необходимость логгировать действия пользователя. Для этого создали синглтон, который использовался в ряде классов. Упомяну, что синглтон использовал jQuery для получения дополнительной информации из DOM. Все шло хорошо до тех пор, пока не понадобилось переиспользовать часть классов в node версии приложения. Эта версия периодически начала падать в ходе тестирования. Оказалось, что в эту версию попали классы, которые использовали логгер синглтон, а в node нет DOM. Эти классы использовали «невидимую» зависимость (причина 2), в результате чего такая ситуация оказалась возможной. Не была заложена возможность подменить логгер на другой (причина 1), из-за чего пришлось переделывать некоторые классы.

Некоторые скажут: «я осознанно использую синглтон, чтобы иметь только единственный экземпляр класса на все приложение». Но создавая синглтон, вы делаете экземпляр класса единственным на все пространство исполнения кода (jvm в java, rhino или V8 в javascript), а не на все приложение. Просто в большинстве случаев внутри пространства исполнения кода находится только одно приложение, и получается, что эти пространства совпадают. Но это не так для тестов. Каждый тест — часть приложения, запущенная в одном пространстве исполнения с другими тестами.

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

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

Рассмотрим такой пример: вам поручают протестировать класс PhoneAccount, отвечающий за операции с телефонным аккаунтом. Недолго думая, вы пишите:

попытка 1
PhoneAccount phoneAccount = new PhoneAccount('79008001020');
phoneAccount.addMoney(100);
expect(phoneAccount.getBalance()).toBe(100);


Запускаете изолированно свой тест и в рантайме получаете ошибку доступа к null. Что пошло не так? Вы спрашиваете у коллеги, писавшего этот класс. Он, долго думая, вспоминает, что нужно инициализировать класс синглтон PhoneAccountTransactionProcessor. Вы думаете: «а как я должен был до этого догадаться?». Затем, добавляете:

попытка 2
PhoneAccountTransactionProcessor.init(...);
PhoneAccount phoneAccount = new PhoneAccount('79008001020');
phoneAccount.addMoney(100);
expect(phoneAccount.getBalance()).toBe(100);


Но опять при запуске получаете ошибку доступа к null. В недоумении вы снова спрашиваете коллегу: «что я делаю не так?». На что получаете ответ: «А ты инициализировал очередь транзакций — AccountTransactionQueue?». Немного раздраженный вы дописываете код, но что-то вам подсказывает, что этим дело не ограничится, и просите не уходить «опытного» коллегу.

попытка 3
AccountTransactionQueue.start(...);
PhoneAccountTransactionProcessor.init(...);
PhoneAccount phoneAccount = new PhoneAccount('79008001020');
phoneAccount.addMoney(100);
expect(phoneAccount.getBalance()).toBe(100);


И снова ошибка. Но не успеваете вы задать вопрос, как коллега выдает: «Ты же не подключил базу транзакций!». Дописываете, глубоко вздыхая:

попытка 4
TransactionsDataBase.connect(...)
AccountTransactionQueue.start(...);
PhoneAccountTransactionProcessor.init(...);
PhoneAccount phoneAccount = new PhoneAccount('79008001020');
phoneAccount.addMoney(100);
expect(phoneAccount.getBalance()).toBe(100);


Наконец, тест проходит.

Думаю, вы уже поняли, что не так с этим кодом — какая-то черная магия. Три класса врут о своих зависимостях, порядок инициализаций должен быть именно таким, хотя код не диктует вам именно такой порядок.

А что если не будет синглтонов, и каждый класс будет требовать все что ему нужно для работы в конструктор?

правильный код
TransactionsDataBase db = new TransactionsDataBase(...);

AccountTransactionQueue transactionQueue;
transactionQueue = new AccountTransactionQueue(db);

PhoneAccountTransactionProcessor transactionProcessor;
transactionProcessor = PhoneAccountTransactionProcessor(transactionQueue);

PhoneAccount phoneAccount = new PhoneAccount('79008001020', transactionProcessor);
phoneAccount.addMoney(100);

expect(phoneAccount.getBalance()).toBe(100);


Теперь нет никакой магии, никаких скрытых каналов общения между классами. Любой новый член команды сможет самостоятельно написать такой тест, потому что он сразу увидит все зависимости. Сам код диктует порядок инициализации, нельзя сделать по-другому. И это огромный плюс, ведь в реальных приложениях могут быть десятки строк с инициализацией классов. И еще одно важное заключение: в таком коде появляется возможность передать null вместо класса (там, где это возможно) либо заменить класс моком. В коде с синглтонами такое было невозможно.

Еще пример:

до: не тестируемый код
class LoginService {
	private static LoginService instance;
	private LoginService() {};
	static LoginService getInstance() {
		if (instance == null) {
			instance = new RealLoginService();
		}
		return instance;
	}
	// вызвать перед началом тестов
	// не использовать за пределами тестов, строго на строго!
	static setForTest(LoginService testDouble) {
		instance = testDouble;
	}
	// вызвать после тестов
	// не использовать за пределами тестов, строго на строго!
	static resetForTest() {
		instance = null;
	}
	// ... 
}

// в другом месте
class AdminDashboard {
	boolean isAuthenticatedAdminUser(User user) {
		LoginService loginService = LoginService.getInstance();
		return loginService.isAuthenticatedAdmin(user);
	}
}

// тесты 

AdminDashboard adminDashboard = new AdminDashboard()
assertTrue(adminDashboard.isAuthenticatedAdminUser(user));
// нет способа подменить LoginService, будет использован настоящий
// жизнь боль


Устраняем синглтон:

после: тестируемый код
class LoginService {
	// ... 
}

// в другом месте
class AdminDashboard {
	AdminDashboard(LoginService loginService) {
	 	this.loginService = loginService;
	}
	boolean isAuthenticatedAdminUser(User user) {
		return this.loginService.isAuthenticatedAdmin(user);
	}
}

// тесты 

AdminDashboard adminDashboard = new AdminDashboard(new MockLoginService());
assertTrue(adminDashboard.isAuthenticatedAdminUser(user));


Теперь можно легко и просто подменять LoginService, а так же не нужны методы «только для тестов».

Исключения, в каких случаях синглтоны все-таки можно использовать:

  • синглтон можно использовать для неизменяемых (immutable) объектов, то есть для тех которые не изменяются в течении жизни приложения. Например: константные настройки
  • синглтон можно использовать, когда объект с информацией не используется в приложении. Например, логгер или google аналитика. Приложение только записывает информацию, но не может прочитать ее. Такие синглтоны безопасны для приложения, поскольку у них как будто нет глобального состояния, это просто труба в никуда. Но если вам необходимо протестировать, что запись в логи действительно происходит, то вам придется поменять зависимости и спускать логгер в конструктор.

Также некоторые системные объекты имеют скрытые глобальные переменные, например math.random и new Date(). Если вы хотите тестировать классы, которые используют такие объекты, то вам необходимо создать свои обертки ними, чтобы иметь возможность подменять их.

Матерый конструктор


Матерый конструктор — конструктор, который делает что-то кроме инициализации полей своего класса. Строго говоря, конструктор должен отвечать только за инициализацию полей класса, а любая другая работа нарушает принцип единой ответственности (Single Responsibility Principle). Такая «лишняя» работа в конструкторе затрудняет его тестирование, так как тест не может передать свои заглушки зависимости в тестируемый класс. Вот типичные паттерны матерых конструкторов:


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

Инициализация аргументов конструктора


до: не тестируемый код
class Metro {
	TicketPrices ticketPrices;
	Metro(TicketPrices ticketPrices) {
		this.ticketPrices = ticketPrices;
		ticketPrices.setCostCalculator(new MoscowCostCalculatorWithVerySlowConstructor());
	}
}

// очень медленный тест

TicketPrices ticketPrices = new TicketPrices();
Metro metro = new Metro(ticketPrices);
expect(metro.isWork()).toBe(true);


Конструктор класса Metro выполняет не свою обязанность — инициализирует аргумент. Такой код плох по многим причинам, но нас интересует тестируемость. Если инициализация будет медленной, то все тесты на Metro будут выполняться долго.

после: тестируемый код
class Metro {
	TicketPrices ticketPrices;
	Metro(TicketPrices ticketPrices) {
		this.ticketPrices = ticketPrices;
	}
}

class TicketPricesFactory {
	TicketPrices build() {
		TicketPrices ticketPrices = new TicketPrices();
		ticketPrices.setCostCalculator(new VerySlowMoscowCostCalculator());	
		return ticketPrices;
	}
}


// test

TicketPrices ticketPrices = new TicketPrices();
ticketPrices.setCostCalculator(null);	
Metro metro = new Metro(ticketPrices);
expect(metro.isWork()).toBe(true);



Теперь в тесте мы можем не создавать не нужные для теста классы, заменяя их на пустышками. Логика создания и инициализации TicketPrices ушла в фабрику.

Условия и циклы


до: не тестируемый код
class Car {
	IEngine engine;
	Car() {
		if (FLAG_ENGINE.get()){
			this.engine = new V8Engine();
		} else {
			this.engine = new V12Engine();
		}
	}
}

// test

// эээм, надо установить FLAG_ENGINE в нужное положение
Car car = new Car();
// тестируем один из двух вариантов настоящего двигателя


Тест завязан на некий флаг. Априори можно протестировать только 2 варианта двигателей.

после: тестируемый код
class Car {
	IEngine engine;
	Car(IEngine engine) {
		this.engine = engine;
	}
}

class EngineFactory {
	IEngine build(boolean isV8) {
		if (isV8){
			return new V8Engine();
		} else {
			return new V12Engine();
		}
	}
}

// создание Сar выглядит так
Car car = new Car(new EngineFactory().build(FLAG_ENGINE.get()));

// test

IEngine simpleEngine = new SimpleEngine();
Car car = new Car(simpleEngine);

// тестируем как хотим, simpleEngine под нашим контролем 
// можно использовать любой двигатель




Перенос части конструктора в другие методы класса


до: не тестируемый код
class Voicemail {
	User user;
	private List<Call> calls;
	Voicemail(User user) {
		this.user = user;
	}
	
	init(Server server) {
		this.calls = server.getCallsFor(this.user);
	}
	
	// ТОЛЬКО ДЛЯ ТЕСТОВ, НЕ ИСПОЛЬЗОВАТЬ!!!
	setCalls(List<Call> calls) {
		this.calls = calls;
	}
	
	// ...
}

// тесты

User dummyUser = new DummyUser();
Voicemail voicemail = new Voicemail(dummyUser);
voicemail.setCalls(buildListOfTestCalls());


В этом примере при помощи метода init, забирающего на себя часть работы конструктора, пытаются облегчить жизнь в тестах. init будет использоваться в боевом коде, а в тестах будут подставляться нужные calls в обход тяжелого вызова init. Казалось бы, ничего плохого тут нет, и тесты можно нормально писать. Но это не выход. Нарушается принцип единой ответственности — за инициализацию класса отвечают несколько мест кода. Это вносит путаницу в код и затрудняет добавление нового функционала в такой класс. К тому же не тестируется метод init. Как правило, наличие таких методов как init, initialize, setup говорит о том, что класс берет на себя слишком много обязанностей. В данном примере класс Voicemail знает о способе получения звонков (server.getCallsFor). Но способ получения звонков не должен интересовать Voicemail:

после: тестируемый код
class Voicemail {
	List<Call> calls;
	Voicemail(List<Call> calls) {
		this.calls = calls;
	}
	
	// ...
}

class ProviderGetCalls {
	List<Call> getCalls(Server server, User user) {
 		return server.getCallsFor(user);
	}
}

// тесты

Voicemail voicemail = new Voicemail(buildListOfTestCalls());

// теперь можно тестировать как угодно




Заключение


Многие принципы, которые были приведены в статье (не использовать оператор new в бизнес коде, требовать все зависимости явно в конструктор), известны давно, и все вместе они образуют принцип Dependency Injection.

Если для вашего языка существует IoC контейнер, то я вас поздравляю — вы избавлены от необходимости писать системный код (фабрики), о котором шла речь выше. За вас эту работу сделает IoC контейнер на основе заданной конфигурации. Именно для этого IoC контейнеры и были придуманы — избавить разработчиков от написания однообразных фабрик.

В языках, для которых нет IoC контейнеров (в основном динамические языки), тем не менее, ничего не мешает применять принцип Dependency Injection. Просто придется вручную писать системный код, что абсолютно не является катастрофой.

В заключение хочу отметить, что всё выше написанное не претендует на полноту охвата данной темы. Все изложенное основано на личном опыте (два крупных проекта), статьях по данной теме (особенно помог «прозреть» блог одного из разработчиков angular Misko Hevery), а также на спорах и общении с коллегами.
Пишете ли вы тестируемый код (не путать с «пишете ли вы тесты»)?

Проголосовало 464 человека. Воздержалось 156 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (72)


  1. flancer
    06.10.2015 14:05
    +1

    // тесты 
    
    LoginService.setForTest(new MockLoginService()); // разве эта операция не дает возможность подменить настоящий LoginService на мок?
    
    AdminDashboard adminDashboard = new AdminDashboard();
    assertTrue(adminDashboard.isAuthenticatedAdminUser(user));
    // нет способа подменить LoginService, будет использован настоящий
    // жизнь боль
    


    1. iKBAHT
      06.10.2015 14:48
      +1

      Да вы правы, не совсем верно, что нет способа подменить, LoginService.setForTest(...) даст возможность. Но такой подход к написанию тестов не является «правильным», о чем говорится в начале статьи:

      Далее под понятием тест подразумевается красивый и чистый unit тест, который написан без различных хаков, без дополнительных «магических» библиотек для подмены зависимостей на лету, и прочего удовольствия, затрудняющего чтение и поддержку тестов.


  1. DancingOnWater
    06.10.2015 16:38
    -12

    Оххх…

    С молоком матери, а вернее еще в C++ должно быть зазубрено, что дело конструктора — выделить память под объект, а вот заполнять ее, т.е. иницилизировать, — не его задача. В идеале он должен провести только минимальную инициализацию, т.е. такую, что при любом первом использовании экземпляра не вылететь. Иницилизация при конструировании удобна, но в C# лучше пользоваться конструкцией

    var house = new House{Kitchen=new Kitchen()};

    Это становиться неудобно если у вас количество полей\проперти больше пятака. Но при этом создавая конструктор с 7-8 аргументами одного типа вы нарываетесь на проблемы. Причин же не инициализировать в конструкторе по минимум две:

    1) Вы имеете шанс нарваться на неожидаемое исключение, а писать что-то вида: try{}cath(Exception e){} — гробить всю логику обработки ошибок

    2) Потенциальная ресурсоемкость и непредсказуемая длительность инициализации. Вот скажите, зачем мне при создании объекта формы тут же тащить данные из БД, подвешивая все и вся? Именно поэтому есть класс init который занимается сложной и тяжелой иницилизацией в специально отведенном ему месте.

    Другой пример. Вам нужно скопировать объект. Что будет происходить если конструктор инициируется в конструкторе: сперва вы заполняете одними значениями. потом поверх них накатываете другие. Скажите: копируете через конструкторы с аргументами иницилизации? — добро пожаловать в отладку при добавлении нового поля\проперти. Скажите что это влияет на скорость выполнения? — А если у вас таких объектов с десяток миллионов?

    Далее, если поля имеют ограничения в доступе, то расширять область доступа к ним через конструктор — нарушение инкапсуляции в рабочем коде. Причем при написании теста в C++ я не стесняюсь писать define private public и очень иногда жалею. что в C# этого не сделать и приходится выкручиваться

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

    Вообще говоря писать тестируемый код просто — просто прежде чем стучать по клавишам надо задаться вопрос: а как я этот код буду контролировать.


    1. dmbreaker
      06.10.2015 19:21
      +1

      Писать тестируемый код просто, если сначала написать тест для него :)


      1. iKBAHT
        06.10.2015 19:57
        +1

        Тестируемый код не обязан иметь тесты. Можно лишь гипотетически представить как будет написан тест на класс. Если представить получилось — значит код тестируемый.


      1. DancingOnWater
        07.10.2015 10:54

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


        1. VolCh
          07.10.2015 10:59

          TDD не исключает предварительную проработку интерфейсов. Более того, она её прямо предполагает.


          1. DancingOnWater
            07.10.2015 11:03

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


    1. Fireball222
      06.10.2015 20:04
      +5

      А почему лучше пользоваться конструкцией?
      var house = new House{Kitchen=new Kitchen()};

      Как минимум можно забыть / не знать про необходимость инициализации этого свойства и получить объект в несогласованном состоянии.


      1. DancingOnWater
        07.10.2015 09:32
        -3

        Это читабельнее, вы, не лезя в код конструктора, видите какие свойства заданы.

        Насчет несогласованности. Если свойства ортогональны, то попадание в несогласованное состояние — баг. В случае, если ортогонализация всех свойств слишком сложна или используем класс в пардигме RAII (последняя в языках со сборщиком мусора создает больше проблем чем решает) — конструктора без входных параметров не должны быть как такового.


        1. Fireball222
          07.10.2015 12:26

          А зачем это вам знать, какие свойства и как заданы? Это ответственность класса, он например может задекорировать какой-либо из переданных параметров прозрачно для вас, как для потребителя.

          Это наверное может быть удобнее в случае каких-либо простых DTO с большим кол-вом необязательных свойств, но пример из статьи явно не об этом.


          1. DancingOnWater
            07.10.2015 13:07

            Эм… давайте начнем с того, что в статье вот такой код, который приводится как нетестируемый:

            class House {
            Kitchen kitchen;
            Bedroom bedroom = new Bedroom();

            House() {
            this.kitchen = new Kitchen(new Refrigerator());
            }

            //…
            }
            Мы видим два момента: конструктор без параметров как таковых и два поля с модифактором доступа по умолчанию. Предлагаемое решение вот это:
            class House {
            Kitchen kitchen;
            Bedroom bedroom;

            House(Kitchen kitchen, Bedroom bedroom) {
            this.kitchen = kitchen;
            this.bedroom = bedroom;
            }

            //…
            }
            Плюс добавляется фабрика.

            Теперь что говорю я:
            По синтаксису C# и Java поля c модификатором доступа по умолчанию имеют ограничение в области видимости. Меняя сигнатуру конструктора вы даете доступ к ним извне. Нарушение инкапсуляции.

            Если же имеется в виду банальная опечатка, то тестируя код, да и в боевом коде лучше сразу видеть какие Поля\Свойства вы задаете, а не выяснять это залезая в код. Для этого есть приведенная мною конструкция.


            1. withkittens
              07.10.2015 13:43

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

              var house = new House { Kitchen = new Kitchen() }; 
              

              Что нам мешает через десять строчек сунуть в house.Kitchen что-нибудь другое? Можно, конечно, сделать write-once property, но вообще это нарушение принципа наименьшего удивления.

              Если зависимости передавать в конструкторе, они точно так же остаются видны, «недоинициализированность» невозможна. Есть и ещё один жирный плюс — IoC-контейнеры. Они очень любят конструкторы (де-факто стандарт реализации DI), а проперти как раз наборот не любят.


              1. DancingOnWater
                07.10.2015 13:57

                >>Я, может, чего-то не увидел, но вот в вашем примере я как раз и наблюдаю нарушение инкапсуляции:
                У меня ее быть не может, потому-что я исхожу из того, что автор примера опечатался и поля имеют модификатор public

                P.S. IoC-контейнеры не юзал, ничего сказать не могу


                1. withkittens
                  07.10.2015 14:16

                  Так у автора же псевдокод, помесь C#, Java и TypeScript. Модификаторы доступа поставляйте мысленно, наиболее адекватные.


                  1. DancingOnWater
                    07.10.2015 14:20

                    На typescript не довелось писать, а вот в C# и Java, как я уже говорил, модификаторы доступа имеют ограничения. Дальше по примеру видим:

                    House house = new House();
                    // ээээм, непонятно как тестировать
                    // нет доступа к kitchen и bedroom

                    Все как и говорил: поля с ограниченным доступом.


                1. VolCh
                  08.10.2015 06:31

                  и поля имеют модификатор public

                  Судя по «нет доступа к kitchen и bedroom» они имеют модификатор private.


            1. withkittens
              07.10.2015 13:51
              +1

              Вы имеете шанс нарваться на неожидаемое исключение
              Не кидайте в конструкторах исключения ;)

              зачем мне при создании объекта формы тут же тащить данные из БД, подвешивая все и вся?
              А правда, зачем? Потребуйте в конструкторе «предоставлятор» данных, который знает, как тащить данные, но не тащит, пока не попросят. Когда данные действительно понадобятся, тогда, пожалуйста, тащите.


              1. DancingOnWater
                07.10.2015 14:03
                -1

                >>Не кидайте в конструкторах исключения ;)
                В принципе невозможно, даже в C++ при нехватке памяти кидается исключение
                >>А правда, зачем?
                И вы предлагаете отложенную или ленивую инициализацию. В любом случае конструктор это уже не делает.


                1. withkittens
                  07.10.2015 14:21

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

                  Вашему классу совершенно всё равно, как и откуда берутся данные. У него есть только провайдер. А берутся ли данные из кэша или их нужно грузить по сети из бд, установлено ли уже соединение с базой, нужно ли перед этим всем пожарить яичницу — это всё ненужные классу детали.


                  1. DancingOnWater
                    07.10.2015 14:41
                    -1

                    >>Ну, нехватка памяти — это в большинстве случаев уже капут. Быстро падаем и не думаем.
                    Или же штатно закрываем зарвавшийся модуль и предлагаем пользователю подождать и не ломать ему всю работу.
                    >>Вашему классу совершенно всё равно, как и откуда берутся данные.
                    И он по-прежнему не делает эту работу в конструкторе. С таким же успехом можно задать где-нибудь проперти типа Source.


            1. VolCh
              08.10.2015 06:34

              тестируя код, да и в боевом коде лучше сразу видеть какие Поля\Свойства вы задаете, а не выяснять это залезая в код

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


              1. DancingOnWater
                08.10.2015 09:29
                -2

                Да елы-палы, хватит меня считать шизофреником! Если я говорю, что доступ к «приватным» полям через конструктор нарушение инкапсуляции, это значит что я ее буду нарушать еще более грубым способом! Нет!

                Я говорил, что если у нас изначально ситуация:

                class House {
                public Kitchen kitchen;
                public Bedroom bedroom = new Bedroom();

                public House() {
                this.kitchen = new Kitchen(new Refrigerator());
                }

                То вместо вот этого:
                class House {
                public Kitchen kitchen;
                public Bedroom bedroom = new Bedroom();

                public House(Kitchen kitchen, Bedroom bedroom ) {

                }
                плюс фабрика, или даже этого:
                class House {
                public Kitchen kitchen;
                public Bedroom bedroom = new Bedroom();

                public House(Kitchen kitchen, Bedroom bedroom ) {

                }
                public House() {
                this.kitchen = new Kitchen(new Refrigerator());
                }

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


                1. VolCh
                  09.10.2015 07:21

                  Не в контейнере дело. Передача параметров в конструктор не нарушает инкапсуляции — конструктор часть публичного интерфейса, мы лишь сообщаем что ему нужно передать такие объекты. Будут ли они присвоены свойствам (и есть ли именно такие свойства, а может другие), проигнорированы или отправлены в ФСБ, мы не знаем. Если присваивать свойства как у вас, то нужно знать об этих свойствах, их имена, типы, тупо не забыть присвоить при создании.


                  1. DancingOnWater
                    09.10.2015 12:59
                    -3

                    Дожили, пишем код и не знаем зачем пишем. А как проверять тогда будем?


                    1. withkittens
                      09.10.2015 14:07

                      Я, если честно, уже не понимаю, о чём спор.

                      public void Test()
                      {
                          var kitchen = GetKitchenMock( ... );
                          var bedroom = GetKitchenMock( ... );
                          var house = new House(kitchen, bedroom);
                      
                          house.Burn();
                      
                          kitchen.IsBurnt.Should().BeTrue();
                          bedroom.IsBurnt.Should().BeTrue();
                      }

                      Тут даже неважно, есть ли вообще у House свойства Kitchen и Bedroom.


                      1. DancingOnWater
                        09.10.2015 14:59
                        -1

                        А нафига вам это тестировать, если поля закрыты?????


                        1. withkittens
                          09.10.2015 15:14

                          Я тестирую корректное взаимодействие дома с кухней и спальней.
                          Ваш напор на закрытость полей мне не понятен. Ну вот я открою поля:

                          class House
                          {
                              public Kitchen Kitchen { get; }
                              public Bedroom Bedroom { get; }
                              ...
                          }
                          

                          В Java свойств нет, но там это решается отдельными геттерами:

                          public Kitchen getKitchen() { return mKitchen; }

                          Что изменилось?


                          1. DancingOnWater
                            09.10.2015 15:18

                            Нарушение инкапсуляции


                            1. withkittens
                              09.10.2015 15:19

                              В чём?


                    1. VolCh
                      12.10.2015 09:23

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


                      1. DancingOnWater
                        12.10.2015 09:54
                        -2

                        а теперь возвращаясь к изначальному вопросу.

                        У нас есть уже готовый класс со своим поведением. Также у нас изначально закрыты поля\свойства. И вся логика объекта этот факт учитывает.

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

                        Вот теперь вопрос: передавая произвольные параметры я точно тестирую нужное мне поведение?


                        1. VolCh
                          12.10.2015 14:47

                          Передавайте не произвольные параметры, а параметры, которые соответствуют тестируемым случаям.


                          1. DancingOnWater
                            12.10.2015 15:09

                            Да каким, если они не подразумевались вовсе?


                            1. VolCh
                              12.10.2015 19:44
                              +1

                              Что значит «не подразумевались»? Написали класс, который неизвестно что делает?


                              1. DancingOnWater
                                13.10.2015 09:30
                                -1

                                Нет, известно, но также известно, что для этой работы ему не нужно подавать на вход ни Kitchen, ни Bedroom.от слова вообще


                                1. VolCh
                                  13.10.2015 11:13

                                  Инстансы не нужно, а вот классы нужно по любому.

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

                                  Вообще лично мне пример решения с House не очень нравится, поскольку мы перенесли инстанцирование в фабрику, но не избавились от зависимости от конкретной реализации, просто перенесли её.


                                  1. DancingOnWater
                                    13.10.2015 11:22
                                    -1

                                    Интересный взгляд, чем-то напоминает СТО или квантовую механику: для пользователя класса инкапсуляция не нарушена, т.к. он не знает что именно делает класс, а вот с точки зрения автора конструктора инкапсуляция нарушена, т.к. он знает что именно делает конструктор.


                                    1. VolCh
                                      13.10.2015 15:44

                                      Для автора не инкапсуляция нарушена, а изменены публичные интерфейсы, контракты и реализация.


                                      1. DancingOnWater
                                        13.10.2015 15:53
                                        -1

                                        Итого: для контроля, надобность в общем случае сомнительна, нам предлагается менять поведение класса. А делается это для того, чтобы протестировать оное.


                                        1. VolCh
                                          13.10.2015 16:39

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


                                          1. DancingOnWater
                                            14.10.2015 10:47
                                            -2

                                            И вот тут нарушаем KISS


                                            1. VolCh
                                              14.10.2015 17:57
                                              +3

                                              Принцип единственной ответственности — прямое следствие KISS. Конструктор класса конструирует свой инстанс — это просто. Конструктор класса конструирует свой инстанс и ещё с пяток других — это сложно.


                                  1. iKBAHT
                                    13.10.2015 12:16
                                    +1

                                    Вообще лично мне пример решения с House не очень нравится, поскольку мы перенесли инстанцирование в фабрику, но не избавились от зависимости от конкретной реализации, просто перенесли её.

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


                                1. withkittens
                                  13.10.2015 13:51

                                  Вам не нравится, что изначально Дом сам создавал себе Кухню и Спальню, а потом эти комнаты стали пихать извне?


                                  1. DancingOnWater
                                    13.10.2015 14:05

                                    Да, но не забудьте, что примеры условны.

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


                                    1. withkittens
                                      13.10.2015 15:00

                                      Вы не видите разницы между буфером с какими-то значениями (= данные) и Кухней со Спальней (= сущности с логикой)?


                                      1. DancingOnWater
                                        13.10.2015 15:07

                                        Сущности с логикой? Где об этом сказано?


                                        1. withkittens
                                          13.10.2015 15:17
                                          +1

                                          вызов любого метода House приведет к вызову kitchen и/или bedroom, а их мы не контролируем. Если они общаются с БД или шлют запросы в сеть, то тесты обречены.
                                          Это раз.

                                          Два, для тестируемости кода в этом случае применили внедрение зависимостей: не House управляет временем жизни Kitchen/Bedroom, а код извне, т.е. использующий House. Внедрение зависимостей и инверсия управления — это про логику. Данные не внедряют.


                                          1. DancingOnWater
                                            13.10.2015 15:39

                                            Ну можно и так понять, но принцип подается-то как универсальный.

                                            По раз: общение приватных данных с БД или по сети без возможности настроить источник возможно в случае каком — правильно, использование синглтона или иные варианты работы через глобальное состояния. И это разговор которой идет чуть ниже.

                                            Два, поля объявлены зависимостями и на основании чего — не понятно. Как они могут быть зависимостями, если методы класса не дают доступ к ним?


                                            1. VolCh
                                              13.10.2015 16:34

                                              Методы не дают, но без этих классов класс House невозможно, как минимум, инстанцировать — он внутри себя обращается к, как минимум, конструкторам этих классов.


                                              1. DancingOnWater
                                                14.10.2015 10:35

                                                Ну и что с того?


                                                1. VolCh
                                                  14.10.2015 17:59
                                                  +1

                                                  А то, что он от них зависит. Скажем, скорость выполнения конструктора House зависит от скорости выполнения конструктора Bedroom.


                                          1. DancingOnWater
                                            13.10.2015 15:44

                                            Хотя, есть вариант как они могут стать зависимостями — это опять ход через глобальное состояние


    1. defuz
      07.10.2015 04:49

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


      1. DancingOnWater
        07.10.2015 11:32
        -3

        Это правда, но в синглетоне им можем адекватно управлять. Адекватный синглетон должен распознать, что он инициируется в тестовом окружении.


        1. iKBAHT
          07.10.2015 12:27
          +6

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


          1. DancingOnWater
            07.10.2015 13:15
            -1

            Это можно сделать и не занося тестовый код в боевой.


        1. defuz
          07.10.2015 17:16
          -1

          Если у вас получается такой сложный синглтон, который умеет распознавать, что он находится в тестовом окружении, и из-за этого, как я понимаю, может инициализироватся каким-то иным образом, может стоит вообще отказатся от идеи использования синглтона?

          Допустим вы хотите сделать коннект с базой данных синглтоном, и хотите, чтобы этот умный синглтон подключался к боевой или тестовой БД в зависимости от окружения. Разве не лучше в таком случае просто инициализировать сервисы, которым нужно подключение к базе данных, передавая им в качестве аргумента объект коннектора, и вообще отказатся от идеи, что этот объект должен быть именно синглтоном? Просто в боевом окружении вы будете инициализировать сервисы с боевым коннектом к БД, а для тестов – с тестовым коннектом. На мой взгляд, такой подоход гибче (можно создавать сколько угодно способов и окружений для работаты с БД, например отдельно для нагрузочного тестирования и для stage), т.к. уменьшает связанность (сервис не завязан строго на конкртеных класс подключения) и избавляет от неявного поведения, заключенном в умном синглтоне (что бы изменить работы сервиса, нужно что-то перещелкнуть в другом месте).


          1. DancingOnWater
            08.10.2015 09:41

            Сразу оговорюсь, что по моему мнению те кто пишет на C# и использует синглтон для доступа к базе заместо Linq to Entity или, если .Net < 4.0, Linq to SQL — того багром и без лишних базаров. Поэтому пример не очень удачный, но давайте оставим тот момент, когда надо использовать этот патерн, а примем за аксиому, что он у нас есть.

            Давайте возьмем класс Settings в C# (статический класс, да не синглтон, но это не суть), который читает настройки из App.config. Для боевого приложения берутся настройки, что лежат в боевом проекте, для теста — в тестовом. Сложно? — Абсолютно нет.

            Более абстрактный пример, но для C++. Синглтон — класс из динамически подгружаемой либы. В боевом варианте система сборки кладет боевой вариант, в тестовом — тестовый. Сложно? — Не более чем все остальное в С++.


            1. defuz
              08.10.2015 19:41

              Мой пример не имеет никакого отношения конктрено к C# и это вымышленный пример, так что не придерайтесь.

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

              С другой стороны, если вы хотите гарантировать уникальность чего-то в рамках приложения и его логики (которую мы и хотели бы потенциально протестировать), то синглтон – плохое решение. Допустим мы пишем WSGI-сервис на Python. Несколько различных сервисов могут сосуществовать в рамках одного процеса, и наоборот – один сервис может форкнутся до нескольких процессов. В обоих случаях из-за синглтона у нас проблемы.


              1. DancingOnWater
                09.10.2015 14:20

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


            1. withkittens
              09.10.2015 14:42

              (статический класс, да не синглтон, но это не суть)
              Суть. Статический класс (а также реализация синглтона в виде Singleton.DefaultInstance) — это implicit dependency. Извне класса (читайте: по конструктору) не узнать, что класс использует какие-то там настройки.

              Отдать управление временем жизни объекта на откуп IoC-контейнеру — сложно? Нет. Сам класс ничего не будет знать о том, в скольких экземплярах его насоздавали (зачем ему это?), а IoC-контейнер может гарантировать существование единственного экземпляра.


              1. DancingOnWater
                09.10.2015 15:04
                -1

                Синглетон с параметрами при создании? Мое удивление стремительно набирает вторую космическую и покидает орбиту старушки Земли.


                1. withkittens
                  09.10.2015 15:09

                  Я нигде не говорил о параметрах.


                  1. DancingOnWater
                    09.10.2015 15:12

                    Тогда я не улавливаю разницу между:
                    >>Статический класс (а также реализация синглтона в виде Singleton.DefaultInstance)
                    и моим: (статический класс, да не синглтон, но это не суть)

                    P.S. Или это как в старой книжке, мне стоило писать (статический класс — да, не синглтон, но это не суть)


                    1. withkittens
                      09.10.2015 15:40

                      Похоже, я начал какую-то мысль, а продолжил другой.
                      Читайте: это всё implicit dependency. И далее по комментарию.


                      1. DancingOnWater
                        09.10.2015 15:48

                        И вот тут я понял, что потерял нить разговора


  1. Gorthauer87
    06.10.2015 17:54

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


  1. dmitryanufriev
    06.10.2015 22:26
    +4

    Добыча знаний напоминает Закон Деметры.


  1. dymanoid
    07.10.2015 21:50

    Не совсем понял, почему

    создание полей класса через new
    плохо.

    Ужас-ужас?

    class Foo 
    {
      private readonly ObservableCollection<string> bars;
      public Foo()
      {
        this.bars = new ObservableCollection<string>();
      }
    }
    


    1. withkittens
      07.10.2015 22:48
      +2

      Исключение:

      оператор new можно использовать в бизнес коде для создания объектов-хранилищ, не содержащих поведения (например, HashMap, Array).