Home Статьи Является ли внутреннее ключевое слово С# запахом кода?

Является ли внутреннее ключевое слово С# запахом кода?

by admin

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

Что такое внутреннее ключевое слово?

В C# внутреннее ключевое слово может использоваться для класса или его членов. Это один из С# access modifierс. Внутренние типы или члены доступны only within files in the same assembly, (C# internal keyword documentation).

Зачем нам нужно внутреннее ключевое слово?

«Обычно внутренний доступ используется при разработке на основе компонентов, потому что он enables a group of components to cooperate in a private manner without being exposed to the rest of the application code. Например, платформа для создания графических пользовательских интерфейсов может предоставлять классы Control и Form, которые взаимодействуют друг с другом, используя элементы с внутренним доступом. Поскольку эти члены являются внутренними, они не подвергаются воздействию кода, использующего фреймворк». (C# internal keyword documentation)

Вот варианты использования, которые я видел для использования внутреннего ключевого слова в члене класса:

  • Вызов частной функции класса в той же сборке.
  • Чтобы протестировать частную функцию, вы можете пометить ее как внутреннюю и предоставить dll тестовой DLL через InternalsVisibleTo.

Оба случая можно рассматривать как code smell, говоря, что эта частная функция должна быть публичной.

Посмотрим несколько примеров

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

class A{
	public void func1(){
		func2();
	}
	private void func2(){}
}

class B{
	public void func(A a){
		a.func2(); //Compilation error 'A.func2()' is inaccessible due to its protection level
	}
}

Решение простое — просто отметьте A::func2 как публичный.

Давайте рассмотрим немного более сложный пример:

public class A{
	public void func1(){}
	private void func2(B b){}
}

internal class B{
	public void func3(A a){		
		a.func2(this); //Compilation error 'A.func2(B)' is inaccessible due to its protection level
	}
}

В чем проблема? просто отметьте func2 как общедоступную, как мы делали это раньше.

public class A{
	public void func1(){ ... }
	public void func2(B b){ ...} // Compilation error: Inconsistent accessibility: parameter type 'B' is less accessible than method 'A.func2(B)' 
}

internal class B{
	public void func3(A a){		
		a.func2(this); 
	}
}

Но мы не можем?. B — это внутренний класс, поэтому он не может быть частью сигнатуры публичной функции публичного класса.

Это решения, которые я нашел, упорядоченные по легкости:

  1. Пометьте функцию внутренним ключевым словом
public class A{
	public void func1(){ }
	internal void func2(B b){}
}

internal class B{
	public void func3(A a){		
		a.func2(this); 
	}
	
}

2. Создайте внутренний интерфейс

internal interface IA2{
	void func2(B b);
}

public class A:IA2{
	public void func1(){
		var b = new B();
		b.func3(this);
	}
	void IA2.func2(B b){} //implement IA2 explicitly because func2 can't be public
}

internal class B{
	public void func3(A a){ 
		((IA2)a).func2(this); //use interface instead of class to access func2	
	}
	
}

3. Извлеките A.func2 в другой внутренний класс и используйте его вместо A.func2.

internal class C{
	public void func2(B b){
	 //extract A:func2 to here
	}
}

public class A{
	public void func1(){}
	private void func2(B b){
		new C().func2(b); 
	}
}

internal class B{
	public void func3(){	//a is no longer needed	
		new C().func2(this); //use internal class instead of private function
	}
	
}

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

Но у нас нет публичных классов, мы используем интерфейсы…

Давайте посмотрим на более реальный пример:

public interface IA{
	void func1();
}

internal class A : IA {
	public void func1(){}
	private void func2(B b){}
}

internal class B{
	public void func3(IA a){		
		a.func2(this);  //Compilation error IA' does not contain a definition for 'func2' and no extension method 'func2' accepting a first argument of type 'IA' could be found

	}	
}

Давайте посмотрим, как предыдущие решения адаптированы к этому примеру:

  1. Пометьте функцию как Internal. это означает, что вам нужно будет привести к классу, чтобы вызвать функцию, поэтому Это будет работать только в том случае, если класс A является единственным, который реализует интерфейс., что означает, что IA не имитируется в тестах, и нет другого производственного класса, который реализует IA.
public interface IA{
	void func1();
}

internal class A : IA {
	public void func1(){}
	internal void func2(B b){}
}

internal class B{
	public void func3(IA a){		
		((A)a).func2(this); //cast to A in order to accses func2

	}	
}

2. Создайте внутренний интерфейс, расширяющий общедоступный интерфейс.

internal interface IExtendedA : IA{
	void func2(B b);
}

public interface IA{
	void func1();
}

internal class A : IExtendedA {
	public void func1(){}
	public void func2(B b){}
}

internal class B{
	public void func3(IExtendedA a){		
		a.func2(this);

	}	
}

3. Извлеките A.func2 в другой внутренний класс и используйте его вместо A.func2.

4. Отделите функцию от внутренних классов и добавьте ее в открытый интерфейс.

Мы видим, что internal keyword is the easiest solution, но есть и другие решения, использующие traditional building blocks of OOP: classes and interfaces. Мы видим, что второе решение — добавить внутренний интерфейс — ненамного сложнее, чем пометить функцию ключевым словом internal.

Почему бы не использовать внутреннее ключевое слово?

Как я показал в предыдущих примерах, используя internal keyword is the easiest solution. Но вам придется нелегко в будущем, если вам нужно будет:

  • Переместите общедоступный класс A в другую DLL (поскольку внутреннее ключевое слово больше не будет применяться к той же dll).
  • Создайте еще один производственный класс, который реализует IA
  • Имитация IA в тестах

Вы можете подумать: «Но это всего лишь одна строка кода, я или кто-либо другой может легко изменить ее, если это необходимо». Now у тебя есть one строка кода, которая выглядит так:

((MyClass)a).internalFunction

но если другим также нужно будет вызвать эту функцию, это line will be copy-pasted around внутри DLL.

Мой вывод

Я думаю, что пометка члена класса внутренним ключевым словом is a code smell. В примерах, которые я показал выше, это easiest решение, НО может вызвать проблемы в будущем. Создание internal interface is almost as easy and more explicit.

Сравните с С++

Ключевое слово «друг» C++ аналогично внутреннему ключевому слову C#. Это позволяет классу или функции получать доступ к закрытым членам класса. Разница в том, что он позволяет получить доступ к specific класс или функция и not all the classes in the same DLL. На мой взгляд, это лучшее решение, чем внутреннее ключевое слово C#.

Дальнейшее чтение

Practical uses for the "internal" keyword in C#
Why does C# not provide the C++ style 'friend' keyword?

ВАМ ТАКЖЕ МОЖЕТ ПОНРАВИТЬСЯ

ОСТАВИТЬ КОММЕНТАРИЙ