Java (13) - Abstraktní třídy a rozhraní

Minule jsme si ukázali, jakým způsob lze v Javě specifikovat podtypy. Tentokrát budeme postupovat opačným směrem – představíme si rozhraní a abstraktní třídy, což jsou konstrukty, které slouží ke generalizaci (zobecnění) tříd.

Abstraktní třídy

Stejně jako v minulém dílu si představme, že máme firmu, kde pracují zaměstnanci mnoha různých profesí (ředitele, sekretářky, uklízečky...). Tito zaměstnanci sdíli určité generické chování, které je společné všem profesím, například všichni zdraví na potkání své kolegy.

Zaměstnanci se ale v určitých aspektech liší. Všichni sice pracují (metoda work()), ale každá profese dělá něco trochu jiného.Minule jsme tuto situaci řešili tak, že jsme vytvořili třídu obecného zaměstnance, ze které dědili všichni specifičtí činitelé.

Tentokrát ovšem v naší firmě pracují pouze specifičtí zaměstnanci – to znamená, že neexistuje (nemůže existovat) žádná instance obecné profese. Z tohoto hlediska by bylo velmi nešikovné implementovat metody společné nadtřídy (když stejně nebudou nikdy využity). Zároveň by toto nijak nezamezilo konstrukci onoho obecného zaměstnance, který v naší firmě nemůže pracovat (z definice příkladu).

Klíčové slovo abstract

Pro řešení této situace použijeme klíčové slovo abstract. Pokud toto slovo vepíšeme do hlavičky třídy, tak z ní nelze vytvářet instance (tím zamezíme vytvoření instance obecného zaměstnance).

Důležitejší je ale jeho použití u metod abstraktních tříd. U takto označených metod můžeme vynechat jejich tělo (a napsat místo něj pouze středník), čímž deklarujeme, že tuto metodu musí mít implementovanou všichni neabstrakní potomci. Tohoto později využijeme při polymorfním volání nad jednotlivými objekty (vizte minulý díl).

Implementace

  /**
   * Abstraktni trida zamestnance
   * @author Pavel Micka
   */
  public abstract class Employee {
      protected String name;
      protected int age;
  
      /**
       * Abstraktni tridy samozrejme maji konstruktor, aby sly zinicializovat
       * jejich fieldy
       * @param name jmeno zamestnance
       * @param age vek zamestnance
       */
      public Employee(String name, int age) {
          this.name = name;
          this.age = age;
      }
  
      /**
       * Necha zamestnance pracovat.
       * Tato metoda je abstraktni - nema telo, jeji implementace bude vynucena
       * v prvni neabstraktni tride, ktera oddedi tuto tridu.
       */
      public abstract void work();
  
      /**
       * Abstrakni tridy taktez mohou mit neabstraktni metody
       */
      public void sayHello(){
          System.out.println("Dobry den, jmenuji se " + name + " a je mi " + age + " let.");
      }
  
      /**
       * @return the age
       */
      public int getAge() {
          return age;
      }
  
      /**
       * @param age the age to set
       */
      public void setAge(int age) {
          this.age = age;
      }
  
      /**
       * @return the name
       */
      public String getName() {
          return name;
      }
  
      /**
       * @param name the name to set
       */
      public void setName(String name) {
          this.name = name;
      }
  }
  
  
  /**
   * Trida reditele 
   * @author malejpavouk
   */
  public class Director extends Employee {
      private Employee[] employees;
  
      public Director(String name, int age, Employee[] employees) {
          super(name, age); //volame konstruktor predka
          this.employees = employees;
      }
  
      @Override //rikame prekladaci, ze prekryvame metodu
      public void work() {
          System.out.println("Prave ridim tyto lidi: ");
          for (int i = 0; i < employees.length; i++) {
              if (i != 0) { //pred prvnim zamestnancem neni ve vypisu carka
                  System.out.print(", ");
              }
              System.out.print(employees[i].getName());
          }
          System.out.println(""); //nakonec odradkujeme
      }
  
      /**
       * @return the employees
       */
      public Employee[] getEmployees() {
          return employees;
      }
  
      /**
       * @param employees the employees to set
       */
      public void setEmployees(Employee[] employees) {
          this.employees = employees;
      }
  }
  
  /**
   * Trida reprezentujici sekretarku
   * @author Pavel Micka
   */
  public class Secretary extends Employee{
  
      public Secretary(String name, int age) {
          super(name, age);
      }
  
      /**
       * Zde je implementace prace zamestnance (v tomto pripade sekretarky)
       */
      @Override
      public void work() {
          System.out.println("Usilovne pracuji, varim kafe, pripravuji materialy, pisu dopisy.");
      }
  
  }
  
  

Volání

      /**
       * @param args the command line arguments
       */
      public static void main(String[] args) {
          Employee em1 = new Secretary("Karolina Krasna", 18);
          Employee em2 = new Secretary("Petra Povolna", 20);
          Employee[] array = {em1, em2}; //vytvorime pole tri zamestnancu
  
          //Objekt reditele
          Employee dir = new Director("Petr Podnikatel", 35, array);
  
          //Pole techto zamestnancu
          Employee[] array2 = {em1, em2, dir};
          
          //for each cyklus pro pruchod pres vsechny prvky kolekce (pole)
          for (Employee employee : array2) {
              /*
               * Pomoci polymorfismu se zavola vzdy ta spravna metoda
               */
              employee.sayHello();
              employee.work();
              System.out.println(""); //odradkujeme po kazdem zamestnance
          }
      }
  
  Dobry den, jmenuji se Karolina Krasna a je mi 18 let.
  Usilovne pracuji, varim kafe, pripravuji materialy, pisu dopisy.
  
  Dobry den, jmenuji se Petra Povolna a je mi 20 let.
  Usilovne pracuji, varim kafe, pripravuji materialy, pisu dopisy.
  
  Dobry den, jmenuji se Petr Podnikatel a je mi 35 let.
  Prave ridim tyto lidi: 
  Karolina Krasna, Petra Povolna
  

Rozhraní (interface)

Druhou velmi podobnou konstrukcí jsou rozhraní (klíčové slovo interface). Základním rozdílem je, že interface obsahuje pouze konstanty a metody bez těla (v tomto případě je neoznačujeme slovem abstract). Rozhraní je kontrakt, který specifikuje operace, které má třída splňovat, a který se již nezabývá tím, jak toho bude konkrétně dosaženo.

Velkou výhodou rozhraní oproti abstraktním třídám je, že každá třída může implementovat až mnoho rozhraní, avšak vždy maximálně jednu třídu.

Zatímto pro dědění tříd (a dědení interfaců mezi sebou) využíváme v hlavičce klíčové slovo extends, tak pro implementaci rozhraní používáme slovo implements.

Konvence

Pro pojmenovávání rozhraní neexistuje žádná široce uznávaná konvence. Časté je využívání přídavných jmen pro rozhraní (Serializable, Movable atp.), což ale nelze v mnoha případech dodržet (neexistuje přídavné jméno, které by onen kontrakt vystihovalo). Další častou konvencí je využívání přípon Interface a Iface. Poslední častou variantou je nepoužít žádnou příponu pro rozhraní, ale pojmenovat třídu implementující rozhraní jeho názvem s příponou Impl (rozhraní: Server, třída: ServerImpl).


Příklad

  package jpz13.example2;
  
  /**
   * Rozhrani pro dopravni prostredky pohubujici se vpred
   * @author Pavel Micka
   */
  public interface Movable {
      /**
       * Rychlostni limit, jimz se mohou dopravni prostedky pohybovat
       */
      public static final int SPEED_LIMIT = 50;
  
      /**
       * Pohyb prostredku vpred
       */
      public void moveForward();
  }
  
  package jpz13.example2;
  
  /**
   * Rozhrani pro dopravni prostredky, ktere mohou i couvat
   * @author Pavel Micka
   */
  public interface Reversible {
      /**
       * Operace couvani
       */
      public void reverse();
  }
  
  package jpz13.example2;
  /**
   * Trida vlaku
   * @author Pavel Micka
   */
  public class Train implements Movable{
      public void moveForward() {
          System.out.println("Pohybuji se vpred rychlosti " + SPEED_LIMIT);
      }
  }
  
  package jpz13.example2;
  
  /**
   * Trida auta, ktere se muze pohybovat vpred a vzad
   * @author Pavel Micka
   */
  public class Car implements Movable, Reversible{
  
      public void moveForward() {
          System.out.println("Pohybuji se vpred rychlosti" + (SPEED_LIMIT - 5));
      }
  
      public void reverse() {
          System.out.println("Couvam rychlosti " + (SPEED_LIMIT/10));
      }
  }
  

Kdy použít abstraktní třídu a kdy rozhraní?

Velmi častou otázkou je, kdy je vhodné použít abstraktní třídu a kdy rozhraní. Ačkoliv jsou si tyto dva konstrukty velmi podobné, tak mají poněkud odlišné využití a navzájem se velmi dobře doplňují. Odpovědí je proto, že buď dává jasný smysl pouze jeden z konstruktů, případně, že použijeme oba dva.


Příklad

Abychom si dnešní látku procvičili, tak si ukážeme jeden příklad z reálného světa. Příklad sice bude velmi zjednodušený, ale jasně nám ukáže základní myšlenku rozhraní a abstraktních tříd.

Mějme dva typy databází – klasické SQL databáze (Oracle, algoritmyQL, MySQL, MS SQL Server atp.) a objektové databáze (např. db4o). Program chceme postavit tak, abychom mohli implementace (v tomto případě operací select a insert) jednoduše zaměňovat (pomocí polymorfismu).

Hierarchie tříd

Z logiky věci vyplývá, že na vrcholu naší hierarchie bude rozhraní definující operace save a insert. Z něj pak budou dědit dvě větve tříd. První větev bude určena pro SQL databáze a jejím vrcholem bude abstraktní třída implementující přikazy select a insert obecným způsobem platným pro značnou část relačních databází. Z této třídu budou dědit jednotlivé implementace pro specifické databáze (a překryjí případně generické chování vlastní implementací).

Druhou větví budou objekty určené přímo pro jednotlivé objektové databáze. Ty bohužel žádnou abstraktní třídu mít nemohou, jelikož nemají žádný standardizovaný dotazovací jazyk.

UML diagram tříd
UML diagram tříd

Implementace

  /**
   * Interface pro operace nad databazi
   * @author Pavel Micka
   */
  public interface DataAccessObjectIface {
      /**
       * Ulozi objekt do databaze
       * @param o
       */
      public void insert(Object o);
      
      /**
       * Vybere z databaze objekt na zaklade identifikatoru
       * @param identifier identifikator zaznamu
       * @param clazz trida, na kterou se dotazujeme
       * @return
       */
      public Object select(int identifier, Class clazz);
  
      /**
       * Vrati stranku zaznamu
       * @param from cislo prvniho zaznamu
       * @param rows pocet zaznamu na strance
       * @param clazz trida, na kterou se dotazujeme
       * @return rows zaznamu pocinaje zaznamem cislo from
       */
      public Object[] selectPage(int from, int rows, Class clazz);
  }
  
  /**
   * Abstraktni trida implementujici operace obecnym zpusobem platným pro značnou část databází
   * @author Pavel Micka
   */
  public abstract class AbstractSqlDao implements DataAccessObjectIface {
  
      public AbstractSqlDao() {
          prepareResources();
      }
  
      /**
       * Tato metoda cini vsechny pripravy pro pouziti teto tridy nad zvolenou
       * databazi. Napriklad vytvari pool spojeni s databazi, vytvari tabulky,
       * pokud neexistuji atp.
       */
      protected abstract void prepareResources();
  
      public void insert(Object o) {
          System.out.println("Generic SQL: Vkladam objekt do SQL databaze obecnym zpusobem");
      }
  
      public Object select(int identifier, Class clazz) {
          System.out.println("Generic SQL: Vytvarim dotaz na radku tabulky " + clazz.getSimpleName() + " s identifikatorem " + identifier + " obecnym zpusobem");
          return "result"; //zde by byl skutecny vystup operace, kdybychom implementovali realnou tridu
      }
  
      public Object[] selectPage(int from, int rows, Class clazz){
          System.out.println("Generic SQL: Vytvarim dotaz obecnym zpusobem na stranku tabulky " + clazz.getSimpleName() + ", prvni zaznam " + from + ", pocet zaznamu: " + rows);
          //v realnem svete bychom museli jednotlive vracene radky premapovat na objekty
          Object[] array = {"Jeden vysledek", "Druhy", "Treti"};
          return array;
      }
  }
  
  
  
  /**
   * DAO pro algoritmyQL, v tomto pripade je svet milosrdny a umoznuje nam kompletne
   * vyuzit obecnych metod abstraktni tridy
   * @author Pavel Micka
   */
  public class algoritmyqlDao extends AbstractSqlDao{
      public algoritmyqlDao() {
          super();
      }
  
      @Override
      protected void prepareResources() {
          System.out.println("algoritmyQL: Pripravuji databazi algoritmyQL");
      }
  }
  
  /**
   * DAO pro MS SQL Server
   * MS SQL Server ma urcita specifika (ty ma koneckoncu asi kazda relacni databaze)
   * V nasem pripade predefinujeme metodu dotazujici se na stranku
   * @author Pavel Micka
   */
  public class MSSqlDao extends AbstractSqlDao{
  
      @Override
      protected void prepareResources() {
          System.out.println("MS SQL: Pripravuji databazi MS SQL Server");
      }
  
      @Override
      public Object[] selectPage(int from, int rows, Class clazz) {
          System.out.println("MS SQL: Vytvarim dotaz specifickym zpusobem pro MS SQL Server na stranku tabulky " + clazz.getSimpleName() + ", prvni zaznam " + from + ", pocet zaznamu: " + rows);
          Object[] result = {"Jeden vysledek", "Druhy", "Treti"};
          return result;
      }
  }
  
  /**
   * DAO pro db4o objektovou databazi
   * @author Pavel Micka
   */
  public class Db4oDao implements DataAccessObjectIface{
  
      public Db4oDao() {
          System.out.println("DB4O: Pripravuji databazi db4o");
      }
  
      public void insert(Object o) {
          System.out.println("DB4O: Vkladam objekt do db4o databaze");
      }
  
      public Object select(int identifier, Class clazz) {
          System.out.println("DB4O: Dotazuji se na objekt typu" + clazz.getSimpleName() + " s indentifikatorem " + identifier + " databaze");
  
          return "result";
      }
  
      public Object[] selectPage(int from, int rows, Class clazz) {
          System.out.println("DB4O: Vytvarim dotaz specifickym zpusobem pro db4o na instance tridy " + clazz.getSimpleName() + ", prvni zaznam " + from + ", pocet zaznamu: " + rows);
  
          Object[] array = {"Jeden vysledek", "Druhy", "Treti"};
          return array;
      }
  }
  

Volání

      public static void main(String[] args){
          DataAccessObjectIface[] dataAccessObjects = {new algoritmyqlDao(), new MSSqlDao(), new Db4oDao()};
          System.out.println(); //odradujeme hlasky o priprave databazi
  
          Employee em = new Secretary("Martina Zelena", 23); //objekt, ktery budeme ukladat do databaze
  
          for(DataAccessObjectIface dao : dataAccessObjects){
              dao.insert(em);
              dao.select(0, em.getClass()); //pomoci volani getClass ziskame tridu objektu
              dao.selectPage(0, 10, em.getClass()); //chceme prvnich deset zaznamu z DB
              System.out.println(); //odradkujeme
          }
      }
  
  algoritmyQL: Pripravuji databazi algoritmyQL
  MS SQL: Pripravuji databazi MS SQL Server
  DB4O: Pripravuji databazi db4o
  
  Generic SQL: Vkladam objekt do SQL databaze obecnym zpusobem
  Generic SQL: Vytvarim dotaz na radku tabulky Secretary s identifikatorem 0 obecnym zpusobem
  Generic SQL: Vytvarim dotaz obecnym zpusobem na stranku tabulky Secretary, prvni zaznam 0, pocet zaznamu: 10
  
  Generic SQL: Vkladam objekt do SQL databaze obecnym zpusobem
  Generic SQL: Vytvarim dotaz na radku tabulky Secretary s identifikatorem 0 obecnym zpusobem
  MS SQL: Vytvarim dotaz specifickym zpusobem pro MS SQL Server na stranku tabulky Secretary, prvni zaznam 0, pocet zaznamu: 10
  
  DB4O: Vkladam objekt do db4o databaze
  DB4O: Dotazuji se na objekt typuSecretary s indentifikatorem 0 databaze
  DB4O: Vytvarim dotaz specifickym zpusobem pro db4o na instance tridy Secretary, prvni zaznam 0, pocet zaznamu: 10
  

Ke stažení

Celý projekt si můžete stáhnout zde.








Doporučujeme

Internet pro vaši firmu na míru