Czy złym zwyczajem jest używanie interfejsu wyłącznie do kategoryzacji?

12

Na przykład:

Że mam zajęcia A, B, C. Mam dwa interfejsy, nazwijmy je IAnimali IDog. IDogdziedziczy po IAnimal. Ai BIDog, chociaż Cnie jest, ale jest IAnimal.

Ważną częścią jest to, że IDognie zapewnia żadnej dodatkowej funkcjonalności. Służy wyłącznie do zezwalania Ai przekazywania B, ale nie Cdo przekazania, jako argumentu dla niektórych metod.

Czy to zła praktyka?

Kendall Frey
źródło
5
@JarrodRoberson, podczas gdy ja się zgadzam, jeśli pracujesz w świecie .Net, utkniesz z nim (lub będziesz działał wbrew ustalonej konwencji, jeśli zrobisz to inaczej).
Daniel B
2
@JarrodRoberson IMHO MyProject.Data.IAnimali MyProject.Data.Animalsą lepsze niż MyProject.Data.Interfaces.Animali MyProject.Data.Implementations.Animal
Mike Koder
1
Chodzi o to, że nie powinno być żadnego powodu, aby się tym przejmować, czy jest to Interfacealbo Implementation, czy w powtarzalnym prefiksie, czy też w przestrzeni nazw, jest to tautologia w obu kierunkach i narusza DRY. Dogto wszystko, o co powinieneś dbać. PitBull extends Dognie potrzebuje też redundancji implementacji, słowo extendsmówi mi wszystko, co muszę wiedzieć, przeczytaj link, który zamieściłem w moim oryginalnym komentarzu.
1
@Jarrod: Przestań mi narzekać. Złóż skargę do zespołu programistów .NET. Gdybym poszedł wbrew standardowi, moi koledzy nie znosiliby moich wnętrzności.
Kendall Frey

Odpowiedzi:

13

Interfejs, który nie ma żadnego członka lub ma dokładnie takie same elementy z innym interfejsem, który jest dziedziczony z tego samego interfejsu o nazwie Interfejs znaczników i używasz go jako znacznika.

Nie jest to zła praktyka, ale interfejs można zastąpić atrybutami (adnotacjami), jeśli używany język go obsługuje.

Mogę śmiało powiedzieć, że nie jest to zła praktyka, ponieważ widziałem intensywne użycie wzoru „Interfejs znacznika” w Orchard Project

Oto próbka z projektu Orchard.

public interface ISingletonDependency : IDependency {}

/// <summary>
/// Base interface for services that may *only* be instantiated in a unit of work.
/// This interface is used to guarantee they are not accidentally referenced by a singleton dependency.
/// </summary>
public interface IUnitOfWorkDependency : IDependency {}

/// <summary>
/// Base interface for services that are instantiated per usage.
/// </summary>
public interface ITransientDependency : IDependency {}

Proszę zapoznać się z interfejsem znacznika .

Świeża krew
źródło
Czy atrybuty / adnotacje obsługiwałyby sprawdzanie czasu kompilacji (używając C # jako języka referencyjnego)? Wiem, że mogę zgłosić wyjątek, jeśli obiekt nie ma atrybutu, ale wzorzec interfejsu wyłapuje błędy w czasie kompilacji.
Kendall Frey,
Jeśli używasz Marker Interface, więc nie masz w ogóle korzyści z kompilacji, sprawdź. Zasadniczo sprawdzasz typ według interfejsu.
Freshblood,
Jestem zmieszany. Ten wzorzec „Interfejs znaczników” zapewnia sprawdzanie czasu kompilacji, ponieważ nie można przekazać Cmetody do metody oczekującej IDog.
Kendall Frey,
Jeśli używasz tego wzoru, więc wykonujesz zadania odbicia. Mam na myśli, że dynamicznie lub statycznie przeprowadzasz kontrole typów. Jestem pewien, że masz coś złego w przypadku użycia, ale nie widziałem, jak go używasz.
Freshblood,
Pozwól, że opiszę to tak, jak to zrobiłem w komentarzu do odpowiedzi Telastyna. np. mam Kennelklasę, która przechowuje listę IDogs.
Kendall Frey,
4

Tak, to zła praktyka w prawie każdym języku.

Nie ma typów do kodowania danych; pomagają udowodnić, że Twoje dane idą tam, gdzie powinny, i zachowują się tak, jak powinny. Jedynym powodem podtypu jest zmiana zachowania polimorficznego (i nie zawsze wtedy).

Ponieważ nie ma potrzeby pisania, implikowane jest to, co może zrobić IDog. Jeden programista myśli jedną rzecz, inny myśli inną i wszystko się psuje. Lub rzeczy, które reprezentuje, zwiększają się, ponieważ potrzebujesz bardziej precyzyjnej kontroli.

[edytuj: wyjaśnienie]

Chodzi mi o to, że robisz jakąś logikę opartą na interfejsie. Dlaczego niektóre metody mogą działać tylko z psami, a inne z jakimkolwiek zwierzęciem? Ta różnica jest umową dorozumianą, ponieważ nie ma rzeczywistego członka, który zapewniłby różnicę; brak rzeczywistych danych w systemie. Jedyną różnicą jest interfejs (stąd komentarz „bez pisania na klawiaturze”), co oznacza, że różni się on między programistami. [/edytować]

Niektóre języki zapewniają mechanizmy cech, które nie są takie złe, ale te zwykle koncentrują się na możliwościach, a nie na wyrafinowaniu. Mam z nimi niewielkie doświadczenie, więc nie mogę porozmawiać z najlepszymi praktykami. W C ++ / Java / C # jednak ... nie dobrze.

Telastyn
źródło
Jestem zmieszany dwoma środkowymi akapitami. Co rozumiesz przez „podtyp”? Korzystam z interfejsów, które są umowami, a nie implementacjami i nie jestem pewien, jak mają zastosowanie twoje komentarze. Co rozumiesz przez „brak pisania”? Co rozumiesz przez „dorozumiany”? IDog może robić wszystko, co potrafi IAnimal, ale nie ma gwarancji, że będzie w stanie zrobić więcej.
Kendall Frey,
Dziękuję za wyjaśnienie. W moim konkretnym przypadku interfejsy nie są zupełnie inne. Można to najlepiej opisać, mówiąc, że mam Kennelklasę, która przechowuje listę IDogs.
Kendall Frey,
@KendallFrey: Albo odrzucasz interfejs (źle), albo tworzysz sztuczne rozróżnienie, które pachnie niepoprawną abstrakcją.
Telastyn
2
@Telastyn - Celem takiego interfejsu jest dostarczenie metadanych
Freshblood,
1
@Telastyn: Jak więc atrybuty mają zastosowanie do sprawdzania czasu kompilacji? AFAIK, w języku C #, atrybuty mogą być używane tylko w czasie wykonywania.
Kendall Frey
2

Znam tylko język C #, więc nie mogę tego powiedzieć ogólnie o programowaniu OO, ale jako przykład w języku C #, jeśli chcesz sklasyfikować klasy, oczywistymi opcjami są interfejsy, właściwość enum lub niestandardowe atrybuty. Zwykle wybieram interfejs bez względu na to, co myślą inni.

if(animal is IDog)
{
}
if(animal.Type == AnimalType.Dog)
{
}
if(animal.GetType().GetCustomAttributes(typeof(DogAttribute), false).Any())
{
}


var dogs1 = animals.OfType<IDog>();
var dogs2 = animals.Where(a => a.Type == AnimalType.Dog);
var dogs3 = animals.Where(a => a.GetType().GetCustomAttributes(typeof(DogAttribute), false).Any());


var dogsFromDb1 = session.Query<IDog>();
var dogsFromDb2 = session.Query<Animal>().Where(a => a.Type == AnimalType.Dog);
var dogsFromDb3 = session.Query<Animal>().Where(a => a.GetType().GetCustomAttributes(typeof(DogAttribute),false).Any());


public void DoSomething(IDog dog)
{
}
public void DoSomething(Animal animal)
{
    if(animal.Type != AnimalType.Dog)
    {
        throw new ArgumentException("This method should be used for dogs only.");
    }
}
public void DoSomething(Animal animal)
{
    if(!animal.GetType().GetCustomAttributes(typeof(DogAttribute), false).Any())
    {
        throw new ArgumentException("This method should be used for dogs only.");
    }
}
Mike Koder
źródło
Ostatnia sekcja kodu jest głównie tym, do czego go używam. Widzę, że wersja interfejsu jest oczywiście krótsza, podobnie jak sprawdzanie czasu kompilacji.
Kendall Frey
Mam podobny przypadek użycia, ale większość deweloperów .net zdecydowanie tego nie zaleca, ale nie zamierzam używać atrybutów
GorillaApe
-1

Nie ma nic złego w posiadaniu interfejsu, który dziedziczy inny interfejs bez dodawania nowych członków, jeśli można oczekiwać, że wszystkie implementacje tego drugiego interfejsu będą w stanie użytecznie robić rzeczy, których niektóre implementacje pierwszego nie mogą. Rzeczywiście, jest to dobry wzorzec, który IMHO powinien był być bardziej używany w takich rzeczach, jak .NET Framework, zwłaszcza że implementator, którego klasa naturalnie spełniałaby ograniczenia wynikające z bardziej pochodnego interfejsu, może to wskazać po prostu przez implementację bardziej pochodnego interfejsu , bez konieczności zmiany kodu implementacyjnego członka ani deklaracji.

Załóżmy na przykład, że mam interfejsy:

interfejs IMaybeMutableFoo {
  Bar Thing {get; set;} // Oraz inne właściwości
  bool IsMutable;
  IImmutableFoo AsImmutable ();
}
interfejs IImmutableFoo: IMaybeMutableFoo {};

IImmutableFoo.AsImmutable()Oczekuje się, że wdrożenie się zwróci; IMaybeMutableFoo.AsImmutable()oczekuje się, że implementacja klasy mutable zwróci nowy głęboko niezmienny obiekt zawierający migawkę danych. Procedura, która chce przechowywać migawkę danych przechowywanych w, IMaybeMutableFoomoże oferować przeciążenia:

public void StoreData (IImmutableFoo ThingToStore)
{
  // Przechowywanie referencji wystarczy, ponieważ ThingToStore jest znany jako niezmienny
}
public void StoreData (IMaybeMutableFoo ThingToStore)
{
  StoreData (ThingToStore.AsImmutable ()); // Wywołaj bardziej szczegółowe przeciążenie
}

W przypadkach, w których kompilator nie wie, że rzeczą do zapisania jest IImmutableFoo, wywoła AsImmutable(). Funkcja powinna szybko wrócić, jeśli element jest już niezmienny, ale wywołanie funkcji przez interfejs nadal zajmuje trochę czasu. Jeśli kompilator wie, że element jest już IImmutableFoo, może pominąć niepotrzebne wywołanie funkcji.

supercat
źródło
Downvoter: Chcesz komentować? Czasami dziedziczenie interfejsu bez dodawania niczego nowego jest głupie, ale to nie znaczy, że nie ma czasów, gdy jest to dobra (i niewykorzystana IMHO) technika.
supercat