Перегрузка метода. Вы можете злоупотреблять этим?

Что может быть лучше при определении нескольких методов, которые возвращают одну и ту же форму данных с разными фильтрами? Явные имена методов или перегруженные методы?

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

явный способ:

public List<Product> GetProduct(int productId) {    // return a List    }
public List<Product> GetProductByCategory(Category category) {    // return a List    }
public List<Product> GetProductByName(string Name ) {    // return a List    }

перегружен путь:

public List<Product> GetProducts() {    // return a List of all products    }
public List<Product> GetProducts(Category category) { // return a List by Category }
public List<Product> GetProducts(string searchString ) { // return a List by search string }

Я понимаю, что вы можете столкнуться с проблемой с похожими сигнатурами, но если вы передаете объекты вместо базовых типов (string, int, char, DateTime и т. Д.), Это будет меньше проблем. Итак... это хорошая идея перегружать метод, чтобы уменьшить количество методов и для ясности, или каждый метод, который фильтрует данные по-разному, имеет свое имя метода?

16 ответов

Решение

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

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

Простое имя метода, такое как GetProducts(), является ясным и понятным, но многое не сказывается.

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

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

Чтобы проиллюстрировать это, я бы с удовольствием использовал перегрузки для метода DeleteFile():

void DeleteFile(string filePath);
void DeleteFile(FileInfo file);
void DeleteFile(DirectoryInfo directory, string fileName);

Однако для ваших примеров я бы использовал отдельные имена:

public IList<Product> GetProductById(int productId) {...}
public IList<Product> GetProductByCategory(Category category) {...}
public IList<Product> GetProductByName(string Name ) {...}

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

// No collisions, even though both methods take int parameters
public IList<Employee> GetEmployeesBySupervisor(int supervisorId);
public IList<Employee> GetEmployeesByDepartment(int departmentId);

Также есть возможность ввести перегрузку для каждой цели:

// Examples for GetEmployees

public IList<Employee> GetEmployeesBySupervisor(int supervisorId);
public IList<Employee> GetEmployeesBySupervisor(Supervisor supervisor);
public IList<Employee> GetEmployeesBySupervisor(Person supervisor);

public IList<Employee> GetEmployeesByDepartment(int departmentId);
public IList<Employee> GetEmployeesByDepartment(Department department);

// Examples for GetProduct

public IList<Product> GetProductById(int productId) {...}
public IList<Product> GetProductById(params int[] productId) {...}

public IList<Product> GetProductByCategory(Category category) {...}
public IList<Product> GetProductByCategory(IEnumerable<Category> category) {...}
public IList<Product> GetProductByCategory(params Category[] category) {...}

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

Наконец, если вы не пишете одноразовый код, вам нужно разрешить другим людям вызывать ваш код с других языков. Похоже, что большинство бизнес-систем в конечном итоге остаются на производстве намного дольше, чем их использование на сегодняшний день. Может случиться так, что код, который использует ваш класс в 2016 году, в конечном итоге будет написан на VB.NET, C# 6.0, F# или что-то совершенно новое, еще не придуманное. Возможно, язык не поддерживает перегрузки.

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

Вы можете злоупотреблять этим? ну да, это правда.

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

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

Мне нравится перегружать мои методы, чтобы потом в intellisense у меня не было миллиона таких же методов. И мне кажется более логичным, что его просто перегружают, а не называют его по-разному дюжину раз.

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

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

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

Возможно, вам нужны некоторые стандарты проекта. Лично я считаю, что перегруженные методы намного легче читать. Если у вас есть поддержка IDE, сделайте это.

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

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

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

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

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

Что касается путаницы, которая может существовать, когда вы хотите перегружать два метода с одинаковой сигнатурой по типу, как в

public List<Employee> GetEmployees(int supervisorId);
public List<Employee> GetEmployees(int departmentId); // Not Allowed !!

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

  public struct EmployeeId 
  { 
      private int empId;
      public int EmployeeId { get { return empId; } set { empId = value; } }
      public EmployeeId(int employeId) { empId = employeeId; }
  }

  public struct DepartmentId 
  {
   // analogous content
  }

 // Now it's fine, as the parameters are defined as distinct types...
 public List<Employee> GetEmployees(EmployeeId supervisorId);
 public List<Employee> GetEmployees(DepartmentId  departmentId);

Краткий взгляд на структуру должен убедить вас, что многочисленные перегрузки являются общепринятым положением дел. Перед лицом множества перегрузок дизайн перегрузок для удобства использования непосредственно рассматривается в разделе 5.1.1 Руководства по проектированию Microsoft Framework (Kwalina and Abrams, 2006). Вот краткая информация об этом разделе:

  • ПЫТАЙТЕСЬ использовать описательные имена параметров, чтобы указать значение по умолчанию, используемое при более коротких перегрузках.

  • ИЗБЕГАЙТЕ произвольно меняющихся имен параметров в перегрузках.

  • ИЗБЕГАЙТЕ быть непоследовательным в упорядочении параметров в перегруженных элементах.

  • НУЖНО сделать только самую длинную виртуальную перегрузку (если требуется расширяемость). Короткие перегрузки должны просто вызывать более длительную перегрузку.

  • НЕ используйте ref или же out параметры для перегрузки членов.

  • Разрешить null быть переданным для необязательных аргументов.

  • НЕ используйте перегрузку членов вместо определения членов с аргументами по умолчанию.

Другой вариант - использовать объект Query для создания "предложения WHERE". Таким образом, у вас будет только один метод, подобный этому:

public List<Product> GetProducts(Query query)

Объект Query содержит условие, выраженное объектно-ориентированным способом. GetProducts получает запрос путем "анализа" объекта Query.

http://martinfowler.com/eaaCatalog/queryObject.html

Я видел чрезмерную перегрузку, когда у вас есть только небольшие различия в аргументах метода. Например:

public List<Product> GetProduct(int productId) { // return a List  }
public List<Product> GetProduct(int productId, int ownerId ) { // return a List  }
public List<Product> GetProduct(int productId, int vendorId, boolean printInvoice) { // return a List  }

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

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

В вашем примере getProductByName - единственный случай, когда явное объяснение может иметь смысл, так как вы можете получить продукт по какой-то другой строке. Эта проблема была вызвана неоднозначностью примитивных типов; getProduct(Name n) может быть лучшим решением для перегрузки в некоторых случаях.

Вы можете использовать Перегрузку столько, сколько хотите. С точки зрения передового опыта также рекомендуется использовать перегрузку, если вы пытаетесь выполнить ту же "операцию" (целостно) над данными. Например, getProduct()

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

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

Если вы используете.Net 3.5+ и вам нужно применить несколько фильтров, вам, вероятно, лучше использовать IQueryable и цепочку, т.е.

GetQuery<Type>().ApplyCategoryFilter(category).ApplyProductNameFilter(productName);

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

public static IQueryable<T> ApplyXYZFilter(this IQueryable<T> query, string filter)
{
     return query.Where(XYZ => XYZ == filter);
} 

Я фанат "явного" способа: дать каждой функции свое имя. Я даже переработал некоторый код, который имел много Add(...) функции в прошлом, чтобы AddRecord(const Record&), AddCell(const Cell&), так далее.

Я думаю, что это помогает избежать некоторых недоразумений, непреднамеренных приведений (по крайней мере, в C++) и предупреждений компилятора, а также улучшает ясность.

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

Да, вы можете злоупотреблять этим. В вашем примере может показаться, что первый и третий, вероятно, вернут один элемент, а второй вернет несколько. Если это правильно, то я бы назвал первый и третий GetProduct и второй GetProducts или GetProductList

если это не так, и все три возвращают несколько (как если бы вы передали ему productID 5, он возвращает любые элементы с 5 в productid или возвращает любые элементы с строковым параметром в его имени), тогда я бы вызвал все три GetProducts или GetProductList и переопределите их все.

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

Как насчет

public IList<Product> GetProducts() { /* Return all. */}

public IList<Product> GetProductBy(int productId) {...}
public IList<Product> GetProductBy(Category category) {...}
public IList<Product> GetProductBy(string Name ) {...}

И так далее?

Другие вопросы по тегам