Apakah ada cara yang elegan untuk membuat setiap metode di kelas dimulai dengan blok kode tertentu?


143

Saya memiliki kelas di mana setiap metode dimulai dengan cara yang sama:

class Foo {
  public void bar() {
    if (!fooIsEnabled) return;
    //...
  }
  public void baz() {
    if (!fooIsEnabled) return;
    //...
  }
  public void bat() {
    if (!fooIsEnabled) return;
    //...
  }
}

Apakah ada cara yang bagus untuk meminta (dan semoga tidak menulis setiap waktu) fooIsEnabledbagian untuk setiap metode publik di kelas?


45
Lihatlah Pemrograman Berorientasi Aspek (khusus sebelum saran ).
Sotirios Delimanolis

15
Berapa banyak metode yang harus Anda gunakan untuk menggunakan boilerplate ini? Sebelum Anda memulai rute memperkenalkan AOP, Anda mungkin ingin mempertimbangkan untuk memiliki sedikit kode berulang ini. Terkadang sedikit salin dan tempel adalah solusi paling sederhana.
bhspencer

51
Saya menduga pengelola masa depan Anda akan lebih bahagia dengan pelat ketel tambahan daripada harus mempelajari kerangka kerja AOP.
bhspencer

7
Jika setiap metode kelas harus melakukan hal yang sama di baris kode pertama mereka maka kita memiliki desain yang buruk.
Tulains Córdova

7
@ user1598390: Pertanyaannya bukan di luar topik di sini, dan tidak ada tentang ruang lingkup Programer yang membuat pertanyaan itu sangat berarti di sana.
Robert Harvey

Jawaban:


90

Saya tidak tahu tentang elegan, tapi di sini ada implementasi yang bekerja menggunakan Java built-in java.lang.reflect.Proxyyang memberlakukan bahwa semua pemanggilan metode Foodimulai dengan memeriksa enabledkeadaan.

main metode:

public static void main(String[] args) {
    Foo foo = Foo.newFoo();
    foo.setEnabled(false);
    foo.bar(); // won't print anything.
    foo.setEnabled(true);
    foo.bar(); // prints "Executing method bar"
}

Foo antarmuka:

public interface Foo {
    boolean getEnabled();
    void setEnabled(boolean enable);

    void bar();
    void baz();
    void bat();

    // Needs Java 8 to have this convenience method here.
    static Foo newFoo() {
        FooFactory fooFactory = new FooFactory();
        return fooFactory.makeFoo();
    }
}

FooFactory kelas:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class FooFactory {

    public Foo makeFoo() {
        return (Foo) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{Foo.class},
                new FooInvocationHandler(new FooImpl()));
    }

    private static class FooImpl implements Foo {
        private boolean enabled = false;

        @Override
        public boolean getEnabled() {
            return this.enabled;
        }

        @Override
        public void setEnabled(boolean enable) {
            this.enabled = enable;
        }

        @Override
        public void bar() {
            System.out.println("Executing method bar");
        }

        @Override
        public void baz() {
            System.out.println("Executing method baz");
        }

        @Override
        public void bat() {
            System.out.println("Executing method bat");
        }

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

        public FooInvocationHandler(FooImpl fooImpl) {
            this.fooImpl = fooImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Foo.class &&
                !method.getName().equals("getEnabled") &&
                !method.getName().equals("setEnabled")) {

                if (!this.fooImpl.getEnabled()) {
                    return null;
                }
            }

            return method.invoke(this.fooImpl, args);
        }
    }
}

Seperti yang telah ditunjukkan oleh orang lain, sepertinya dibutuhkan terlalu banyak untuk apa yang Anda butuhkan jika Anda hanya memiliki sedikit metode untuk dikhawatirkan.

Yang mengatakan, pasti ada manfaatnya:

  • Pemisahan perhatian tertentu tercapai, karena Fooimplementasi metode tidak perlu khawatir tentang masalah enabledlintas sektoral. Sebaliknya, kode metode hanya perlu khawatir tentang apa tujuan utama metode ini, tidak lebih.
  • Tidak ada cara bagi pengembang yang tidak bersalah untuk menambahkan metode baru ke Fookelas dan keliru "lupa" untuk menambahkan enabledcek. The enabledperilaku cek secara otomatis diwarisi oleh metode baru ditambahkan.
  • Jika Anda perlu menambahkan masalah lintas sektoral lainnya, atau jika Anda perlu meningkatkan enabledpemeriksaan, sangat mudah untuk melakukannya dengan aman dan di satu tempat.
  • Sangat menyenangkan bahwa Anda bisa mendapatkan perilaku seperti AOP ini dengan fungsionalitas Java bawaan. Anda tidak dipaksa harus mengintegrasikan beberapa kerangka kerja lain seperti Spring, meskipun mereka pasti bisa menjadi pilihan yang baik juga.

Agar adil, beberapa kerugiannya adalah:

  • Beberapa kode implementasi yang menangani pemanggilan proxy jelek. Beberapa juga akan mengatakan bahwa memiliki kelas batin untuk mencegah instantiasi FooImplkelas adalah jelek.
  • Jika Anda ingin menambahkan metode baru Foo, Anda harus membuat perubahan dalam 2 titik: kelas implementasi dan antarmuka. Bukan masalah besar, tapi masih sedikit lebih banyak pekerjaan.
  • Doa proxy tidak gratis. Ada overhead kinerja tertentu. Untuk penggunaan umum, itu tidak akan terlihat. Lihat di sini untuk informasi lebih lanjut.

EDIT:

Komentar Fabian Streitel membuat saya berpikir tentang 2 gangguan dengan solusi saya di atas yang, saya akui, saya tidak senang dengan diri saya sendiri:

  1. Pawang panggilan menggunakan string ajaib untuk melewati "diaktifkan-periksa" pada metode "getEnabled" dan "setEnabled". Ini dapat dengan mudah dipecahkan jika nama metode refactored.
  2. Jika ada kasus di mana metode baru perlu ditambahkan yang seharusnya tidak mewarisi perilaku "diaktifkan-periksa", maka itu bisa sangat mudah bagi pengembang untuk mendapatkan kesalahan ini, dan setidaknya, itu berarti menambahkan lebih banyak sihir string.

Untuk menyelesaikan poin # 1, dan untuk setidaknya meringankan masalah dengan poin # 2, saya akan membuat anotasi BypassCheck(atau yang serupa) yang bisa saya gunakan untuk menandai metode di Fooantarmuka yang saya tidak ingin melakukan " mengaktifkan cek ". Dengan cara ini, saya tidak memerlukan string sihir sama sekali, dan itu menjadi jauh lebih mudah bagi pengembang untuk menambahkan metode baru dengan benar dalam kasus khusus ini.

Menggunakan solusi anotasi, kode akan terlihat seperti ini:

main metode:

public static void main(String[] args) {
    Foo foo = Foo.newFoo();
    foo.setEnabled(false);
    foo.bar(); // won't print anything.
    foo.setEnabled(true);
    foo.bar(); // prints "Executing method bar"
}

BypassCheck anotasi:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BypassCheck {
}

Foo antarmuka:

public interface Foo {
    @BypassCheck boolean getEnabled();
    @BypassCheck void setEnabled(boolean enable);

    void bar();
    void baz();
    void bat();

    // Needs Java 8 to have this convenience method here.
    static Foo newFoo() {
        FooFactory fooFactory = new FooFactory();
        return fooFactory.makeFoo();
    }
}

FooFactory kelas:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class FooFactory {

    public Foo makeFoo() {
        return (Foo) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{Foo.class},
                new FooInvocationHandler(new FooImpl()));
    }

    private static class FooImpl implements Foo {

        private boolean enabled = false;

        @Override
        public boolean getEnabled() {
            return this.enabled;
        }

        @Override
        public void setEnabled(boolean enable) {
            this.enabled = enable;
        }

        @Override
        public void bar() {
            System.out.println("Executing method bar");
        }

        @Override
        public void baz() {
            System.out.println("Executing method baz");
        }

        @Override
        public void bat() {
            System.out.println("Executing method bat");
        }

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

        public FooInvocationHandler(FooImpl fooImpl) {
            this.fooImpl = fooImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Foo.class
                    && !method.isAnnotationPresent(BypassCheck.class) // no magic strings
                    && !this.fooImpl.getEnabled()) {

                return null;
            }

            return method.invoke(this.fooImpl, args);
        }
    }
}

11
Saya mengerti bahwa ini adalah solusi cerdas tetapi apakah Anda benar-benar akan menggunakan ini?
bhspencer

1
adalah solusi yang bagus, Anda menggunakan pola proxy dinamis untuk menghias objek dengan perilaku umum yang ditemukan di awal setiap metode.
Victor

11
@ Bhspencer: Pertanyaan yang sangat sah. Saya sebenarnya telah menggunakannya berkali-kali untuk melakukan penanganan pengecualian, logging, penanganan transaksi, dll. Saya akui bahwa untuk kelas yang lebih kecil, sepertinya terlalu banyak, dan mungkin sangat baik. Tetapi jika saya berharap kelas tumbuh banyak dalam kompleksitas, dan ingin memastikan perilaku yang konsisten di semua metode saat saya melakukan itu, saya tidak keberatan dengan solusi ini.
sstan

1
Bukan untuk menjadi bagian dari 97% kejahatan di sini, tetapi apa implikasi kinerja kelas proksi ini?
corsiKa

5
@corsiKa: Pertanyaan bagus. Tidak ada keraguan bahwa menggunakan proxy dinamis lebih lambat daripada panggilan metode langsung. Namun untuk penggunaan umum, overhead kinerja tidak akan terlihat. Utas SO terkait jika Anda tertarik: Biaya kinerja proksi dinamis Java
sstan

51

Ada banyak saran bagus .. apa yang dapat Anda lakukan untuk mengatasi masalah Anda adalah berpikir dalam Pola Negara dan menerapkannya.

Lihatlah potongan kode ini .. mungkin ini akan membuat Anda mendapatkan ide. Dalam skenario ini sepertinya Anda ingin memodifikasi seluruh metode implementasi berdasarkan keadaan internal objek. Harap diingat bahwa jumlah metode dalam objek diketahui sebagai perilaku.

public class Foo {

      private FooBehaviour currentBehaviour = new FooEnabledBehaviour (); // or disabled, or use a static factory method for getting the default behaviour

      public void bar() {
        currentBehaviour.bar();
      }
      public void baz() {
        currentBehaviour.baz();
      }
      public void bat() {
        currentBehaviour.bat();
      }

      public void setFooEnabled (boolean fooEnabled) { // when you set fooEnabel, you are changing at runtime what implementation will be called.
        if (fooEnabled) {
          currentBehaviour = new FooEnabledBehaviour ();
        } else {
          currentBehaviour = new FooDisabledBehaviour ();
        }
      }

      private interface FooBehaviour {
        public void bar();
        public void baz();
        public void bat();
      }

      // RENEMBER THAT instance method of inner classes can refer directly to instance members defined in its enclosing class
      private class FooEnabledBehaviour implements FooBehaviour {
        public void bar() {
          // do what you want... when is enabled
        }
        public void baz() {}
        public void bat() {}

      }

      private class FooDisabledBehaviour implements FooBehaviour {
        public void bar() {
          // do what you want... when is desibled
        }
        public void baz() {}
        public void bat() {}

      }
}

Harap Anda menyukainya!

PD: Apakah implementasi Pola Negara (juga dikenal sebagai Strategi tergantung pada konteks .. tetapi prinsip-prinsipnya sama).


1
OP ingin tidak harus mengulangi baris kode yang sama di awal setiap metode dan solusi Anda melibatkan pengulangan baris kode yang sama di awal setiap metode.
Tulains Córdova

2
@ user1598390 tidak perlu mengulang evalution, di dalam FooEnabledBehaviour Anda berasumsi bahwa klien objek ini telah menetapkan fooEnabled menjadi true, jadi tidak perlu cheking. Hal yang sama berlaku untuk kelas FooDisabledBehaviour. Periksa lagi, kode di dalamnya.
Victor

2
Terima kasih @ bayou.io, mari kita tunggu sampai jawaban OP. Saya pikir komunitas ini melakukan pekerjaan yang berat di sini, ada banyak tips bagus di sini!
Victor

2
Setuju dengan @dyesdyes Saya tidak bisa membayangkan mengimplementasikan ini untuk apa pun kecuali kelas yang sangat sepele. Itu terlalu bermasalah, mengingat bahwa bar()dalam FooEnabledBehaviordan bar()dalam FooDisabledBehaviormungkin berbagi banyak kode yang sama, dengan bahkan mungkin satu baris berbeda di antara keduanya. Anda bisa dengan sangat mudah, terutama jika kode ini dipertahankan oleh junior devs (seperti saya), berakhir dengan kekacauan besar yang tidak dapat dipertahankan dan tidak dapat diuji. Itu bisa terjadi dengan kode apa pun, tetapi ini kelihatannya sangat mudah untuk gagal sangat cepat. +1, karena saran yang bagus.
Chris Cirefice

1
Mmmmm ... saya tidak teman .. tapi pertama, terima kasih atas komentarnya. Bagi saya ukuran kode tidak menjadi masalah selama itu "bersih" dan "dapat dibaca". Dalam mendukung saya, saya harus berpendapat bahwa saya tidak menggunakan kelas eksternal .. yang seharusnya membuat hal-hal lebih mudah diakses. Dan jika ada beberapa perilaku umum, mari kita enkapsulasi dalam CommonBehaviourClass dan delegasikan ke tempat yang dibutuhkan. Dalam buku GOF (bukan buku favorit saya untuk belajar, tetapi memiliki resep yang baik) Anda menemukan contoh ini: en.wikipedia.org/wiki/… . Apakah kurang lebih sama dengan yang saya lakukan di sini.
Victor

14

Ya, tapi ini sedikit pekerjaan, jadi itu tergantung seberapa penting bagi Anda.

Anda dapat mendefinisikan kelas sebagai antarmuka, menulis implementasi delegasi, dan kemudian menggunakan java.lang.reflect.Proxyuntuk mengimplementasikan antarmuka dengan metode yang melakukan bagian bersama dan kemudian memanggil delegate dengan syarat.

interface Foo {
    public void bar();
    public void baz();
    public void bat();
}

class FooImpl implements Foo {
    public void bar() {
      //... <-- your logic represented by this notation above
    }

    public void baz() {
      //... <-- your logic represented by this notation above
    }

    // and so forth
}

Foo underlying = new FooImpl();
InvocationHandler handler = new MyInvocationHandler(underlying);
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
     new Class[] { Foo.class },
     handler);

Anda MyInvocationHandlerdapat terlihat seperti ini (penanganan kesalahan dan perancah kelas dihilangkan, dengan asumsi fooIsEnableddidefinisikan di suatu tempat yang dapat diakses):

public Object invoke(Object proxy, Method method, Object[] args) {
    if (!fooIsEnabled) return null;
    return method.invoke(underlying, args);
}

Tidak terlalu cantik. Tetapi tidak seperti berbagai komentator, saya akan melakukannya, karena saya pikir pengulangan adalah risiko yang lebih penting daripada kepadatan jenis ini, dan Anda akan dapat menghasilkan "rasa" kelas nyata Anda, dengan pembungkus yang agak sulit dipahami ini ditambahkan pada sangat lokal hanya dalam beberapa baris kode.

Lihat dokumentasi Java untuk detail tentang kelas proxy dinamis.


14

Pertanyaan ini terkait erat dengan pemrograman berorientasi aspek . AspectJ adalah ekstensi AOP dari Java dan Anda dapat melihatnya untuk mendapatkan beberapa ispirasi.

Sejauh yang saya tahu tidak ada dukungan langsung untuk AOP di Jawa. Ada beberapa pola GOF yang terkait dengannya, seperti misalnya Metode dan Strategi Templat tetapi tidak akan benar-benar menyelamatkan Anda dari baris kode.

Di Jawa dan sebagian besar bahasa lain, Anda dapat menentukan logika berulang yang Anda butuhkan dalam fungsi dan mengadopsi apa yang disebut pendekatan pengkodean disiplin di mana Anda memanggilnya pada waktu yang tepat.

public void checkBalance() {
    checkSomePrecondition();
    ...
    checkSomePostcondition();
}

Namun ini tidak sesuai dengan kasus Anda karena Anda ingin kode yang difaktorkan keluar untuk dapat kembali checkBalance. Dalam bahasa yang mendukung makro (seperti C / C ++) Anda bisa mendefinisikan checkSomePreconditiondan checkSomePostconditionsebagai makro dan mereka hanya akan diganti oleh preprocessor sebelum kompiler bahkan dipanggil:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java tidak memiliki ini di luar kotak. Ini mungkin menyinggung seseorang, tetapi saya memang menggunakan pembuatan kode otomatis dan mesin templat untuk mengotomatiskan tugas pengkodean berulang di masa lalu. Jika Anda memproses file Java Anda sebelum mengompilasinya dengan preprocessor yang sesuai, misalnya Jinja2, Anda bisa melakukan sesuatu yang mirip dengan apa yang mungkin dalam C.

Kemungkinan pendekatan Java murni

Jika Anda mencari solusi Java murni, apa yang Anda temukan mungkin tidak akan ringkas. Tapi, itu masih bisa memfaktorkan bagian umum dari program Anda dan menghindari duplikasi kode dan bug. Anda dapat melakukan sesuatu seperti ini (ini semacam strategi yang terinspirasi pola). Perhatikan bahwa dalam C # dan Java 8, dan dalam bahasa lain yang fungsinya sedikit lebih mudah ditangani, pendekatan ini mungkin terlihat bagus.

public interface Code {
    void execute();
}

...

public class Foo {
  private bool fooIsEnabled;

  private void protect(Code c) {
      if (!fooIsEnabled) return;
      c.execute();
  }

  public void bar() {
    protect(new Code {
      public void execute() {
        System.out.println("bar");
      }
    });
  }

  public void baz() {
    protect(new Code {
      public void execute() {
        System.out.println("baz");
      }
    });
  }

  public void bat() {
    protect(new Code {
      public void execute() {
        System.out.println("bat");
      }
    });
  }
}

Agak skenario dunia nyata

Anda mengembangkan kelas untuk mengirim bingkai data ke robot industri. Robot membutuhkan waktu untuk menyelesaikan suatu perintah. Setelah perintah selesai, ia mengirimkan Anda kembali bingkai kontrol. Robot dapat rusak jika menerima perintah baru ketika sebelumnya masih dieksekusi. Program Anda menggunakan DataLinkkelas untuk mengirim dan menerima bingkai ke dan dari robot. Anda perlu melindungi akses ke DataLinkinstance.

Panggilan benang antarmuka pengguna RobotController.left, right, upatau downketika pengguna mengklik tombol, tetapi juga panggilan BaseController.ticksecara berkala, untuk perintah forwarding mengaktifkan kembali ke pribadi DataLinkmisalnya.

interface Code {
    void ready(DataLink dataLink);
}

class BaseController {
    private DataLink mDataLink;
    private boolean mReady = false;
    private Queue<Code> mEnqueued = new LinkedList<Code>();

    public BaseController(DataLink dl) {
        mDataLink = dl;
    }

    protected void protect(Code c) {
        if (mReady) {
            mReady = false;
            c.ready(mDataLink);
        }
        else {
            mEnqueue.add(c);
        }
    }

    public void tick() {
        byte[] frame = mDataLink.readWithTimeout(/* Not more than 50 ms */);

        if (frame != null && /* Check that it's an ACK frame */) {
          if (mEnqueued.isEmpty()) {
              mReady = true;
          }
          else {
              Code c = mEnqueued.remove();
              c.ready(mDataLink);
          }
        }
    }
}

class RobotController extends BaseController {
    public void left(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'left' by amount */);
        }});
    }

    public void right(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'right' by amount */);
        }});
    }

    public void up(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'up' by amount */);
        }});
    }

    public void down(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'down' by amount */);
        }});
    }
}

4
Bukankah ini hanya menendang kaleng di jalan. Artinya pengelola masa depan dulu harus ingat untuk jika (! FooIsEnabled) kembali; pada awal setiap fungsi dan sekarang mereka harus ingat untuk melindungi (Kode baru {... pada awal setiap fungsi. Bagaimana ini membantu?
bhspencer

Saya suka Anda analisis dan konteks latar belakang damix911 ... saya akan membangun contoh kode baru pada waktu kompilasi (menggunakan anggota statis pribadi) dengan asumsi bahwa kode tidak akan berubah dari waktu ke waktu dan mengubah nama perubahan untuk "mengeksekusi jika" meneruskan sebagai argumen suatu Kondisi (Suka kelas Predikat) dan Kode. Tetapi itu lebih merupakan perasaan dan penebangan pribadi.
Victor

1
@ Bhspencer Kelihatannya agak canggung di Jawa, dan dalam kebanyakan kasus strategi ini benar-benar rekayasa yang berlebihan dari kode sederhana. Tidak banyak program dapat mengambil manfaat dari pola seperti itu. Namun yang keren adalah kami membuat simbol baru protectyang lebih mudah untuk digunakan kembali dan didokumentasikan. Jika Anda memberi tahu pengelola masa depan bahwa kode penting harus dilindungi protect, Anda sudah memberi tahu dia apa yang harus dilakukan. Jika aturan perlindungan berubah, kode baru masih dilindungi. Ini persis alasan di balik mendefinisikan fungsi tetapi OP perlu "mengembalikan return" yang fungsi tidak bisa lakukan.
damix911

11

Saya akan mempertimbangkan refactoring. Pola ini sangat merusak pola KERING (Jangan ulangi sendiri). Saya percaya ini melanggar tanggung jawab kelas ini. Tetapi ini tergantung pada kendali Anda terhadap kode. Pertanyaan Anda sangat terbuka - di mana Anda menelepon Foocontoh?

Saya kira Anda memiliki kode seperti

foo.bar(); // does nothing if !fooEnabled
foo.baz(); // does also nothing
foo.bat(); // also

mungkin Anda harus menyebutnya seperti ini:

if (fooEnabled) {
   foo.bat();
   foo.baz();
   ...
}

Dan tetap bersih. Misalnya, masuk:

this.logger.debug(createResourceExpensiveDump())

a logger tidak bertanya pada dirinya sendiri , apakah debug diaktifkan. Itu hanya log.

Sebagai gantinya, kelas panggilan perlu memeriksa ini:

if (this.logger.isDebugEnabled()) {
   this.logger.debug(createResourceExpensiveDump())
}

Jika ini adalah perpustakaan dan Anda tidak dapat mengontrol panggilan kelas ini, lempar IllegalStateExceptionyang menjelaskan mengapa, jika panggilan ini ilegal dan menyebabkan masalah.


6
Ini jelas lebih sederhana dan lebih mudah di mata. Tetapi jika tujuan OP adalah untuk memastikan bahwa, ketika metode baru ditambahkan, bahwa logika yang diaktifkan tidak pernah dilewati, maka refactoring ini tidak membuatnya lebih mudah untuk menegakkannya.
sstan

4
Juga untuk contoh log Anda, saya akan mengatakan ini melibatkan banyak pengulangan - setiap kali Anda ingin login, Anda harus memeriksa apakah logger diaktifkan. Saya cenderung mencatat lebih banyak baris daripada jumlah metode di kelas mana pun ...
T. Kiley

4
Ini memecah modularitas, karena sekarang pemanggil harus tahu sesuatu tentang internal foo (dalam hal ini apakah fooEnabled). Ini adalah contoh klasik di mana mengikuti aturan praktik terbaik tidak akan menyelesaikan masalah karena konflik aturan. (Saya masih berharap seseorang akan datang dengan jawaban "mengapa saya tidak memikirkan itu?"
Ian Goldby

2
Yah, itu sangat tergantung pada konteks, apakah itu masuk akal.
Ian Goldby

3
Pencatatan adalah contoh persisnya, di mana saya tidak menginginkan pengulangan dalam kode saya. Saya hanya ingin menulis LOG.debug ("...."); - Dan logger harus memeriksa apakah saya benar-benar ingin men-debug. - Contoh lain adalah penutupan / pembersihan. - Jika saya menggunakan AutoClosable, saya tidak ingin pengecualian jika sudah ditutup, seharusnya tidak melakukan apa-apa.
Falco

6

IMHO solusi paling elegan dan berkinerja terbaik untuk ini adalah memiliki lebih dari satu implementasi Foo, bersama dengan metode pabrik untuk membuat satu:

class Foo {
  protected Foo() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do something
  }

  public static void getFoo() {
    return fooEnabled ? new Foo() : new NopFoo();
  }
}

class NopFoo extends Foo {
  public void bar() {
    // Do nothing
  }
}

Atau variasi:

class Foo {
  protected Foo() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do something
  }

  public static void getFoo() {
    return fooEnabled ? new Foo() : NOP_FOO;
  }

  private static Foo NOP_FOO = new Foo() {
    public void bar() {
      // Do nothing
    }
  };
}

Seperti yang ditunjukkan oleh sstan, lebih baik menggunakan antarmuka:

public interface Foo {
  void bar();

  static Foo getFoo() {
    return fooEnabled ? new FooImpl() : new NopFoo();
  }
}

class FooImpl implements Foo {
  FooImpl() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do something
  }
}

class NopFoo implements Foo {
  NopFoo() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do nothing
  }
}

Adaptasikan ini dengan keadaan Anda yang lain (apakah Anda membuat Foo baru setiap kali atau menggunakan kembali contoh yang sama, dll.)


1
Ini tidak sama dengan jawaban Konrad. Saya suka ini, tapi saya pikir akan lebih aman jika, daripada menggunakan warisan kelas, Anda menggunakan antarmuka seperti yang orang lain sarankan dalam jawaban mereka. Alasannya sederhana: Terlalu mudah bagi pengembang untuk menambahkan metode Foo, dan lupa untuk menambahkan versi no-op dari metode dalam kelas yang diperluas, sehingga mem-bypass perilaku yang diinginkan.
sstan

2
@ Konstanta Kau benar, itu akan lebih baik. Saya melakukannya seperti ini untuk memodifikasi contoh asli kristina sesedikit mungkin untuk menghindari gangguan, tetapi ini relevan. Saya akan menambahkan saran Anda ke jawaban saya.
Pepijn Schmitz

1
Saya pikir Anda melewatkan satu poin. Anda menentukan apakah isFooEnableddi Instansiasi Foo. Masih terlalu dini. Dalam kode asli, ini dilakukan ketika suatu metode dijalankan. Nilai isFooEnableddapat berubah sementara itu.
Nicolas Barbulesco

1
@NicolasBarbulesco Anda tidak tahu itu, fooIsEnabled bisa menjadi konstan. Atau dapat digunakan dengan cara yang membuatnya konstan secara efektif. Atau itu bisa diatur di beberapa tempat yang terdefinisi dengan baik sehingga akan mudah untuk mendapatkan instance baru Foo setiap waktu. Atau bisa diterima untuk mendapatkan instance Foo baru setiap kali digunakan. Anda tidak tahu, itu sebabnya saya menulis: "sesuaikan ini dengan keadaan Anda yang lain."
Pepijn Schmitz

@PepijnSchmitz - Tentu saja, fooIsEnabled mungkin konstan. Tetapi tidak ada yang memberitahu kita bahwa itu konstan. Jadi saya mempertimbangkan kasus umum.
Nicolas Barbulesco

5

Saya punya pendekatan lain: punya a

interface Foo {
  public void bar();
  public void baz();
  public void bat();
}

class FooImpl implements Foo {
  public void bar() {
    //...
  }
  public void baz() {
    //...
  }
  public void bat() {
    //...
  }
}

class NullFoo implements Foo {
  static NullFoo DEFAULT = new NullFoo();
  public void bar() {}
  public void baz() {}
  public void bat() {}
}

}

dan kemudian Anda bisa melakukannya

(isFooEnabled ? foo : NullFoo.DEFAULT).bar();

Mungkin Anda bahkan dapat mengganti isFooEnableddengan Foovariabel yang FooImpldapat digunakan untuk atau NullFoo.DEFAULT. Maka panggilan itu lebih sederhana lagi:

Foo toBeUsed = isFooEnabled ? foo : NullFoo.DEFAULT;
toBeUsed.bar();
toBeUsed.baz();
toBeUsed.bat();

BTW, ini disebut "pola Null".


Pendekatan menyeluruh itu baik, tetapi menggunakan ungkapan (isFooEnabled ? foo : NullFoo.DEFAULT).bar();itu agak canggung. Miliki implementasi ketiga yang mendelegasikan ke salah satu implementasi yang ada. Alih-alih mengubah nilai bidang isFooEnabled, target delegasi dapat diubah. Ini mengurangi jumlah cabang dalam kode
SpaceTrucker

1
Tetapi Anda mendeportasi memasak internal kelas Foodalam kode panggilan! Bagaimana kita bisa tahu isFooEnabled? Ini adalah bidang internal di kelas Foo.
Nicolas Barbulesco

3

Dalam pendekatan fungsional yang serupa dengan jawaban @ Colin, dengan fungsi lambda Java 8 , dimungkinkan untuk membungkus fitur bersyarat beralih mengaktifkan / menonaktifkan kode ke metode penjaga ( executeIfEnabled) yang menerima tindakan lambda, yang kode yang akan dijalankan secara kondisional dapat berlalu.

Meskipun dalam kasus Anda, pendekatan ini tidak akan menyimpan baris kode apa pun, dengan MENINGGALKAN ini, Anda sekarang memiliki opsi untuk memusatkan perhatian fitur beralih lainnya, ditambah AOP atau masalah debugging seperti pencatatan, diagnostik, pembuatan profil, dkk.

Salah satu manfaat menggunakan lambdas di sini adalah bahwa penutupan dapat digunakan untuk menghindari perlunya membebani executeIfEnabledmetode.

Sebagai contoh:

class Foo {
    private Boolean _fooIsEnabled;

    public Foo(Boolean isEnabled) {
        _fooIsEnabled = isEnabled;
    }

    private void executeIfEnabled(java.util.function.Consumer someAction) {
        // Conditional toggle short circuit
        if (!_fooIsEnabled) return;

        // Invoke action
        someAction.accept(null);
    }

    // Wrap the conditionally executed code in a lambda
    public void bar() {
        executeIfEnabled((x) -> {
            System.out.println("Bar invoked");
        });
    }

    // Demo with closure arguments and locals
    public void baz(int y) {
        executeIfEnabled((x) -> {
            System.out.printf("Baz invoked %d \n", y);
        });
    }

    public void bat() {
        int z = 5;
        executeIfEnabled((x) -> {
            System.out.printf("Bat invoked %d \n", z);
        });
    }

Dengan tes:

public static void main(String args[]){
    Foo enabledFoo = new Foo(true);
    enabledFoo.bar();
    enabledFoo.baz(33);
    enabledFoo.bat();

    Foo disabledFoo = new Foo(false);
    disabledFoo.bar();
    disabledFoo.baz(66);
    disabledFoo.bat();
}

Juga mirip dengan pendekatan Damix, tanpa perlu antarmuka dan implementasi kelas anonim dengan metode override.
StuartLC

2

Seperti yang ditunjukkan dalam jawaban lain, Pola Desain Strategi adalah pola desain yang tepat untuk diikuti untuk menyederhanakan kode ini. Saya telah mengilustrasikannya di sini menggunakan metode doa melalui refleksi, tetapi ada sejumlah mekanisme yang dapat Anda gunakan untuk mendapatkan efek yang sama.

class Foo {

  public static void main(String[] args) {
      Foo foo = new Foo();
      foo.fooIsEnabled = false;
      foo.execute("bar");
      foo.fooIsEnabled = true;
      foo.execute("baz");
  }

  boolean fooIsEnabled;

  public void execute(String method) {
    if(!fooIsEnabled) {return;}
    try {
       this.getClass().getDeclaredMethod(method, (Class<?>[])null).invoke(this, (Object[])null);
    }
    catch(Exception e) {
       // best to handle each exception type separately
       e.printStackTrace();
    }
  }

  // Changed methods to private to reinforce usage of execute method
  private void bar() {
    System.out.println("bar called");
    // bar stuff here...
  }
  private void baz() {
    System.out.println("baz called");
    // baz stuff here...
  }
  private void bat() {
    System.out.println("bat called");
    // bat stuff here...
  }
}

Harus berurusan dengan refleksi agak canggung, jika sudah ada kelas yang melakukan ini untuk Anda, seperti yang sudah disebutkan Proxy.
SpaceTrucker

Bagaimana kamu bisa melakukannya foo.fooIsEnabled ...? A priori, ini adalah bidang internal objek, kita tidak bisa, dan tidak mau, melihatnya di luar.
Nicolas Barbulesco

2

Kalau saja java sedikit lebih baik dalam fungsional. Menurutnya solusi yang paling OOO adalah membuat kelas yang membungkus fungsi tunggal sehingga disebut hanya ketika foo diaktifkan.

abstract class FunctionWrapper {
    Foo owner;

    public FunctionWrapper(Foo f){
        this.owner = f;
    }

    public final void call(){
        if (!owner.isEnabled()){
            return;
        }
        innerCall();
    }

    protected abstract void innerCall();
}

dan kemudian mengimplementasikan bar, bazdan batsebagai kelas anonim yang diperluas FunctionWrapper.

class Foo {
    public boolean fooIsEnabled;

    public boolean isEnabled(){
        return fooIsEnabled;
    }

    public final FunctionWrapper bar = new FunctionWrapper(this){
        @Override
        protected void innerCall() {
            // do whatever
        }
    };

    public final FunctionWrapper baz = new FunctionWrapper(this){
        @Override
        protected void innerCall() {
            // do whatever
        }
    };

    // you can pass in parms like so 
    public final FunctionWrapper bat = new FunctionWrapper(this){
        // some parms:
        int x,y;
        // a way to set them
        public void setParms(int x,int y){
            this.x=x;
            this.y=y;
        }

        @Override
        protected void innerCall() {
            // do whatever using x and y
        }
    };
}

Ide lain

Gunakan solusi nullgl glglgl tetapi kelas make FooImpldan NullFookelas dalam (dengan konstruktor pribadi) dari kelas di bawah ini:

class FooGateKeeper {

    public boolean enabled;

    private Foo myFooImpl;
    private Foo myNullFoo;

    public FooGateKeeper(){
        myFooImpl= new FooImpl();
        myNullFoo= new NullFoo();
    }

    public Foo getFoo(){
        if (enabled){
            return myFooImpl;
        }
        return myNullFoo;
    }  
}

dengan cara ini Anda tidak perlu khawatir mengingat untuk digunakan (isFooEnabled ? foo : NullFoo.DEFAULT).


Katakanlah Anda memiliki: Foo foo = new Foo()untuk menelepon barAnda akan menulisfoo.bar.call()
Colin

1

Sepertinya kelas tidak melakukan apa-apa ketika Foo tidak diaktifkan jadi mengapa tidak mengungkapkan ini pada tingkat yang lebih tinggi di mana Anda membuat atau mendapatkan contoh Foo?

class FooFactory
{
 static public Foo getFoo()
 {
   return isFooEnabled ? new Foo() : null;
 }
}
 ...
 Foo foo = FooFactory.getFoo();
 if(foo!=null)
 {
   foo.bar();
   ....
 }     

Ini hanya berfungsi jika isFooEnabled adalah konstanta. Secara umum, Anda dapat membuat anotasi sendiri.


Konrad, dapatkah Anda mengembangkannya pada anotasi?
Nicolas Barbulesco

Kode asli menentukan apakah fooIsEnabledsuatu metode dipanggil. Anda melakukan ini sebelum instantiasi dari Foo. Masih terlalu dini. Nilai dapat berubah sementara itu.
Nicolas Barbulesco

Saya pikir Anda melewatkan satu poin. A priori, isFooEnabledadalah bidang contoh Fooobjek.
Nicolas Barbulesco

1

Saya tidak terbiasa dengan sintaksis Java. Asumsi bahwa di Jawa, ada polimorfisme, properti statis, kelas abstrak & metode:

    public static void main(String[] args) {
    Foo.fooIsEnabled = true; // static property, not particular to a specific instance  

    Foo foo = new bar();
    foo.mainMethod();

    foo = new baz();
    foo.mainMethod();

    foo = new bat();
    foo.mainMethod();
}

    public abstract class Foo{
      static boolean fooIsEnabled;

      public void mainMethod()
      {
          if(!fooIsEnabled)
              return;

          baMethod();
      }     
      protected abstract void baMethod();
    }
    public class bar extends Foo {
        protected override baMethod()
        {
            // bar implementation
        }
    }
    public class bat extends Foo {
        protected override baMethod()
        {
            // bat implementation
        }
    }
    public class baz extends Foo {
        protected override baMethod()
        {
            // baz implementation
        }
    }

Siapa bilang yang diaktifkan adalah properti statis kelas?
Nicolas Barbulesco

Apa yang new bar()seharusnya berarti?
Nicolas Barbulesco

Di Jawa, kami menulis kelas Namedengan huruf kapital.
Nicolas Barbulesco

Ini perlu terlalu banyak mengubah kode panggilan. Kami memanggil metode normal: bar(). Jika Anda perlu mengubahnya, Anda akan menemui ajal.
Nicolas Barbulesco

1

Pada dasarnya Anda memiliki bendera yang jika diatur, panggilan fungsi harus dilewati. Jadi saya pikir solusi saya akan konyol, tetapi ini dia.

Foo foo = new Foo();

if (foo.isEnabled())
{
    foo.doSomething();
}

Berikut ini adalah implementasi dari Proxy sederhana, jika Anda ingin menjalankan beberapa kode sebelum menjalankan fungsi apa pun.

class Proxy<T>
{
    private T obj;
    private Method<T> proxy;

    Proxy(Method<T> proxy)
    {
        this.ojb = new T();
        this.proxy = proxy;
    }

    Proxy(T obj, Method<T> proxy)
    {
        this.obj = obj;
        this.proxy = proxy;
    }

    public T object ()
    {
        this.proxy(this.obj);
        return this.obj;
    }
}

class Test
{
    public static void func (Foo foo)
    {
        // ..
    }

    public static void main (String [] args)
    {
        Proxy<Foo> p = new Proxy(Test.func);

        // how to use
        p.object().doSomething();
    }
}

class Foo
{
    public void doSomething ()
    {
        // ..
    }
}

Untuk blok kode pertama Anda, Anda perlu metode yang terlihat isEnabled(). A priori, diaktifkan adalah memasak internal Foo, tidak terbuka.
Nicolas Barbulesco

Kode panggilan tidak bisa , dan tidak mau , tahu apakah objek tersebut diaktifkan .
Nicolas Barbulesco

0

Ada solusi lain, menggunakan delegate (pointer to function). Anda dapat memiliki metode unik yang pertama melakukan validasi dan kemudian memanggil metode yang relevan sesuai dengan fungsi (parameter) untuk dipanggil. Kode C #:

internal delegate void InvokeBaxxxDelegate();

class Test
{
    private bool fooIsEnabled;

    public Test(bool fooIsEnabled)
    {
        this.fooIsEnabled = fooIsEnabled;
    }

    public void Bar()
    {
        InvokeBaxxx(InvokeBar);
    }

    public void Baz()
    {
        InvokeBaxxx(InvokeBaz);
    }

    public void Bat()
    {
        InvokeBaxxx(InvokeBat);
    }

    private void InvokeBaxxx(InvokeBaxxxDelegate invoker)
    {
        if (!fooIsEnabled) return;
        invoker();
    }

    private void InvokeBar()
    {
        // do Invoke bar stuff
        Console.WriteLine("I am Bar");
    }

    private void InvokeBaz()
    {
        // do Invoke bar stuff
        Console.WriteLine("I am Baz");
    }

    private void InvokeBat()
    {
        // do Invoke bar stuff
        Console.WriteLine("I am Bat");
    }
}

2
Benar, ini ditandai Java dan ini sebabnya saya menekankan dan menulis "Kode dalam C #" karena saya tidak tahu Java. Karena ini adalah pertanyaan Pola Desain sehingga bahasa tidak penting.
ehh

Oh! Saya mengerti, maaf untuk itu, hanya berusaha membantu dan menemukan solusi. Terima kasih
ehh
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.