Какво означава отделяне на два класа на ниво интерфейс?

Да кажем, че имаме клас A в пакет A и клас B в пакет B. Ако обект от клас A има препратка към клас B, тогава се казва, че двата класа имат връзка помежду си.

За да се обърне внимание на свързването, се препоръчва да се дефинира интерфейс в пакет A, който се реализира от клас в пакет B. Тогава обектът от клас A може да се отнася към интерфейса в пакет A. Това често е пример за "инверсия на зависимостта".

Това ли е примерът за "отделяне на два класа на ниво интерфейс". Ако отговорът е да, как се премахва връзката между класовете и се запазва същата функционалност, когато два класа са били свързани?


person Sarit Adhikari    schedule 11.06.2015    source източник
comment
Силно препоръчвам публикациите на Robert C. Martin за SOLID правилата на обектно-ориентираното програмиране. Ето някои подробности за вашия проблем: objectmentor.com/resources/articles/dip.pdf   -  person Maciej Baranowski    schedule 11.06.2015


Отговори (4)


Нека създадем фиктивен пример за два класа A и B.

Клас A в пакет packageA:

package packageA;

import packageB.B;

public class A {
    private B myB;
    
    public A() {
        this.myB = new B();
    }
    
    public void doSomethingThatUsesB() {
        System.out.println("Doing things with myB");
        this.myB.doSomething();
    }
}

Клас B в пакет packageB:

package packageB;

public class B {
    public void doSomething() {
        System.out.println("B did something.");
    }
}

Както виждаме, A зависи от B. Без B, A не може да се използва. Казваме, че A е тясно свързано с B. Ами ако искаме да заменим B в бъдеще с BetterB? За целта създаваме интерфейс Inter в рамките на packageA:

package packageA;

public interface Inter {
    public void doSomething();
}

За да използваме този интерфейс, ние

  • import packageA.Inter; и нека B implements Inter в B и
  • Заменете всички срещания на B в рамките на A с Inter.

Резултатът е тази модифицирана версия на A:

package packageA;

public class A {
    private Inter myInter;
    
    public A() {
        this.myInter = ???; // What to do here?
    }
    
    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

Вече можем да видим, че зависимостта от A до B е изчезнала: import packageB.B; вече не е необходим. Има само един проблем: не можем да създадем екземпляр на интерфейс. Но Инверсията на управление идва на помощ: вместо да инстанцира нещо от тип Inter в рамките на конструктора на A, конструкторът ще изисква нещо, което implements Inter като параметър:

package packageA;

public class A {
    private Inter myInter;
    
    public A(Inter myInter) {
        this.myInter = myInter;
    }
    
    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

С този подход вече можем да променим конкретната реализация на Inter в рамките на A по желание. Да предположим, че напишем нов клас BetterB:

package packageB;

import packageA.Inter;

public class BetterB implements Inter {
    @Override
    public void doSomething() {
        System.out.println("BetterB did something.");
    }
}

Сега можем да инстантираме As с различни Inter-имплементации:

Inter b = new B();
A aWithB = new A(b);
aWithB.doSomethingThatUsesInter();

Inter betterB = new BetterB();
A aWithBetterB = new A(betterB);
aWithBetterB.doSomethingThatUsesInter();

И не трябваше да променяме нищо в рамките на A. Кодът вече е отделен и можем да променим конкретната реализация на Inter по желание, стига договорът(ите) на Inter да е(са) удовлетворен(и). Най-вече можем да поддържаме код, който ще бъде написан в бъдеще и ще прилага Inter.


Допълнение

Написах този отговор през 2015 г. Въпреки че като цяло бях доволен от отговора, винаги съм мислил, че нещо липсва и мисля, че най-накрая знам какво е то. Следното не е необходимо за разбиране на отговора, но има за цел да предизвика интерес у читателя, както и да предостави някои ресурси за по-нататъшно самообразование.

В литературата този подход е известен като Принцип на разделяне на интерфейса и принадлежи към SOLIDпринципи. Има хубав разговор от чичо Боб в YouTube (интересното е с дължина около 15 минути) показвайки как полиморфизмът и интерфейсите могат да бъдат използвани, за да позволят на зависимостта по време на компилиране да сочи срещу потока на контрол (препоръчително е преценката на зрителя, чичо Боб ще се изрече леко за Java). Това, в замяна, означава, че изпълнението на високо ниво не трябва да знае за изпълнението на по-ниско ниво, когато те са отделени чрез интерфейси. Така по-ниските нива могат да се разменят по желание, както показахме по-горе.

person Turing85    schedule 11.06.2015
comment
+1 Inversión of control идва на помощ. Това е най-доброто отделяне и инверсия на контролната връзка, което някога съм виждал. И съм чел книги на тази тема. - person AFP_555; 29.04.2016
comment
Но клас А сега не зависи ли от интерфейса? Току-що го отделихте от B и го свързахте с интерфейса, нали? Как това е по-добро? - person 11m0; 16.08.2017
comment
Всеки разработчик може да внедри интерфейса и да го инжектира чрез инверсия на контрола, без да е необходимо да променя горния код. Вярно е, че Aзависи от Inter и следователно промените в Inter могат да нарушат целия код, изпълняващ Inter. Ето защо интерфейсите трябва да бъдат проектирани внимателно. Има някои възможности за добавяне на методи към интерфейс по-късно, напр. чрез методи по подразбиране. - person Turing85; 16.08.2017
comment
@Turing85 Благодаря. Добре обяснено. - person Muhammad Maqsood; 15.10.2017
comment
Благодаря ти много. Това е едно от най-ясните обяснения в интернет за тази концепция. - person Zeecitizen; 08.06.2018

Представете си, че функционалността на B е да записва журнал в някаква база данни. Класът B зависи от функционалността на клас DB и предоставя някакъв интерфейс за неговата функционалност за регистриране към други класове.

Клас A се нуждае от функцията за регистриране на B, но не го интересува къде се записва регистрационният файл. Не се интересува от DB, но тъй като зависи от B, зависи и от DB. Това не е много желателно.

Така че това, което можете да направите, е да разделите класа B на два класа: абстрактен клас L, описващ функционалността за регистриране (и не в зависимост от DB), и изпълнението в зависимост от DB.

След това можете да отделите класа A от B, защото сега A ще зависи само от L. B сега също зависи от L, затова се нарича инверсия на зависимости, защото B предоставя функционалността, предлагана в L.

Тъй като A сега зависи само от слаб L, можете лесно да го използвате с друг механизъм за регистриране, без да зависи от DB. напр. можете да създадете прост регистратор, базиран на конзола, прилагайки интерфейса, дефиниран в L.

Но тъй като сега A не зависи от B, а (в източниците) само от абстрактния интерфейс L по време на изпълнение, той трябва да бъде настроен да използва някаква специфична реализация на L (B например). Така че трябва да има някой друг, който да каже на A да използва B (или нещо друго) по време на изпълнение. И това се нарича инверсия на контрол, защото преди A реши да използва B, но сега някой друг (напр. контейнер) казва на A да използва B по време на изпълнение.

person Gregor Raýman    schedule 11.06.2015

Ситуацията, която описвате, премахва зависимостта, която клас A има от конкретното изпълнение на клас B, и го замества с интерфейс. Сега клас A може да приеме всеки обект, който е от тип, който имплементира интерфейса, вместо да приема само клас B. Дизайнът запазва същата функционалност, защото клас B е създаден да имплементира този интерфейс.

person Bill the Lizard    schedule 11.06.2015

Това е мястото, където DI (Dependency Injection) рамките наистина блестят.

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

Например...

Вашият ServiceA ще изгради своята логика около интерфейса на ServiceB и не трябва да се тревожи какво се случва под капака на ServiceB.

Това ви позволява да създавате множество реализации на ServiceB, без да се налага да променяте логиката в ServiceA.

За пример

interface ServiceB { void doMethod() }

Можете да взаимодействате със ServiceB в ServiceA, без да знаете какво се случва под капака на ServiceB.

class ServiceAImpl {

    private final ServiceB serviceB;
    
    public ServiceAImpl(ServiceBImpl serviceBImpl) {
        this.serviceB = serviceBImpl
    }

    public void doSomething() {
        serviceB.doMethod(); // calls ServiceB interface method.
    }

}

Сега, тъй като сте създали ServiceA с помощта на договора, посочен в ServiceB, вие можете да промените изпълнението, както искате.

Можете да се подиграете на услугата, да създадете различна логика на свързване към различни бази данни, да създадете различна логика за изпълнение. Всичко това може да се промени и изобщо няма да повлияе на начина, по който ServiceA взаимодейства с ServiceB.

По този начин се постига слабо свързване с IoC (инверсия на контрола). Вече имате модулна и фокусирана кодова база.

person shinjw    schedule 03.09.2020