Pull to refresh

Extension methods: stop the madness!

Reading time5 min
Views6.7K
Давным-давно, когда компьютеры были большими, программисты — бородатыми, а код — процедурным, на Земле царила идиллия. Программисты писали простой и понятный код, не задумываясь о соответствии его догмам. Да и не было тогда никаких догм. Каждый из этих одиноких ковбоев был творцом в своём собственном мире. Каждый выражал свою мысль элегантно и ёмко; каждая строка кода была произведением искусства, достойным безграничного восхищения. Иначе и быть не могло: вычислительные ресурсы были настолько скудны, что никому и в голову не приходило потратить их только на то, чтобы код выглядел красиво.

Однако время шло, и непобедимая машина технического прогресса неумолимо наращивала производительность железа. В один прекрасный день люди поняли, что они могут писать неоптимизированный код, и им за это ничего не будет! Вычислительные мощности выросли настолько, что компьютерам было уже всё равно, что выполнять: выверенный до последней машинной инструкции код, или код, написанный студентом-первокурсником за сосиску в тесте. Это был поистине переломный момент, после которого, как грибы после дождя, стали появляться новые концепции и методики программирования.

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

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

public class Cat
{
	public float CuddlynessFactor { get; private set; }
	
	public Cat(float cuddlynessFactor)
	{
		this.CuddlynessFactor = cuddlynessFactor;
	}

	public void Purr()
	{
		Console.WriteLine("Purr!");
	}

	public void HitTheWall()
	{
		Console.WriteLine("Fu**ing meow!!!");
	}
}

public class Dog
{
	public float TeethSharpness { get; private set; };
	
	public Dog(float teethSharpness)
	{
		this.TeethSharpness = teethSharpness;
	}

	public void Bark()
	{
		Console.WriteLine("Bark!");
	}
	
	public void Bite(Human target)
	{
		target.Leg.DoDamage(DamageLevel.Substantial);
	}
}


Как видите, мы определили два простейших класса: Cat и Dog. Каждый из этих классов является репрезентацией объекта из реальной жизни и обладает некоторыми свойствами этих объектов, релевантными в контексте нашей системы. Эти объекты умеют каким-то образом взаимодействовать с окружающей средой (методы) и обладают какими-то качествами (поля). Суть инкапсуляции заключается в том, что каждый объект содержит в себе все методы и данные, необходимые для его функционирования. Кроме того, объекты не содержат методов или свойств, которые не имеют отношения к их поведению (ещё бы, какой собаке придёт в голову разогнаться по паркету и на полной скорости впилиться в стену?).

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

public static class Transmogrificator
{
	public static Cat DogToCat(Dog dog)
	{
		return new Cat(Math.Sqrt(dog.TeethSharpness) * 42);
	}
}


Вуаля! Используя новый класс, мы с лёгкостью можем превратить собаку в кошку!

public void Test()
{
	var uberDog = new Dog(float.Infinity);
	var testHuman = new Human();
	
	uberDog.Bark();
	uberDog.Bite(testHuman);
	
	var cat = Transmogrificator.DogToCat(dog);
	
	cat.HitTheWall();
}


Sweet! И вот теперь мы вплотную подходим к концепции Extension methods, которую Microsoft не так давно ввела в .NET. По задумке авторов, extension methods должны применяться там, где целевой класс не имеет какой-то функциональности, которая ему очевидно нужна (или, по крайней мере, может являться его частью). Вот что говорит по поводу методов расширения в. и у. MSDN:

In general, you will probably be calling extension methods far more often than implementing your own.
[...]
In general, we recommend that you implement extension methods sparingly and only when you have to. Whenever possible, client code that must extend an existing type should do so by creating a new type derived from the existing type.
[...]
When using an extension method to extend a type whose source code you cannot change, you run the risk that a change in the implementation of the type will cause your extension method to break.


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

Теперь давайте посмотрим, что случится, если мы модифицируем наш пример, сделав метод DogToCat методом расширения.

public void Test()
{
	var uberDog = new Dog(float.Infinity);
	var testHuman = new Human();
	
	uberDog.Bark();
	uberDog.Bite(testHuman);
	
	var cat = dog.DogToCat();
	
	cat.HitTheWall();
}


Казалось бы, ничего страшного не произошло. Ан нет! Теперь при прочтении кода создаётся полное впечатление, что метод DogToCat является неотъемлемой частью типа Dog. Чтобы понять подвох, нужно дождаться подсказки IntelliSense или перейти к непосредственной реализации метода. Давайте разберёмся, почему такая ситуация некошерна:
  • Нарушается один из основополагающих принципов ООП — инкапсуляция. Способность превратиться в кошку — это не способность, принадлежащая собаке. Это способность доброго доктора с бензопилой и в халате, заляпанном кровью. Апологеты методов расширения будут говорить, что формально инкапсуляция не нарушена, так как реализация метода по-прежнему находится в статическом классе. Однако представьте себя на месте нового разработчика, который разбирается с чужим кодом. Для него совершенно логичным будет заключить, что метод DogToCat принадлежит именно типу Dog! Вы только представьте горечь разочарования, которую он испытает, когда узнает, что на самом деле это extension method. Вы когда-нибудь чувствовали себя обманутым? Возможно, бедолаге даже потребуются услуги психотерапевта.
  • Непонятно, где находятся различные методы расширения, предназначенные для одного и того же класса. Непонятно, как их подключать. В то время, как при использовании статического класса у нас есть хорошо сформированное имя, которое обычно отражает логику работы этого класса, для методов расширения у нас есть только догадки. Чтобы подключить нужный метод, нам нужно прописать namespace, имя которого может оказаться совсем неочевидным. Да, я слышу вас, поклонники R#. Я и сам очень люблю этот инструмент, но в данном случае он является лишь обезболивающим, которое маскирует боль в ноге, поражённой гангреной. Изначальная неочевидность подключения методов расширения кагбэ намекает нам, но мы предпочитаем не слушать доводов разума и вместо этого закидываемся очередной дозой морфина.

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

UPD: Видимо, растекшись мыслью по древу, я недостаточно чётко выразил суть статьи. А суть заключается вот в чём: я призываю не использовать Extension methods для расширения функционала классов, находящихся в том же проекте. Написать расширение для класса в скомпиленной сборке, к исходникам которой у вас нет доступа — это хорошо. Применять расширения для создания LINQ-подобного синтаксиса — это очень хорошо. А вот создавать классы, и в этом же проекте навешивать на них дополнительный функционал с помощью Extensions — это очень, очень плохо.
Tags:
Hubs:
-1
Comments40

Articles