Có một cách thanh lịch để làm cho mọi phương thức trong một lớp bắt đầu với một khối mã nhất định không?


143

Tôi có một lớp trong đó mọi phương thức bắt đầu theo cùng một cách:

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

Có một cách hay để yêu cầu (và hy vọng không viết mỗi lần) fooIsEnabledphần cho mọi phương thức công khai trong lớp không?


45
Nhìn vào lập trình hướng đối tượng (cụ thể là trước khi tư vấn ).
Sotirios Delimanolis

15
Có bao nhiêu phương pháp để bạn sử dụng cái nồi này? Trước khi bạn đi xuống tuyến đường giới thiệu AOP, bạn có thể muốn xem xét chỉ cần có một chút mã lặp lại này. Đôi khi một chút sao chép và dán là giải pháp đơn giản nhất.
bhspencer

51
Tôi nghi ngờ người bảo trì trong tương lai của bạn sẽ hạnh phúc hơn với tấm nồi hơi phụ hơn là phải học một khung AOP.
bhspencer

7
Nếu mọi phương thức của lớp phải làm điều tương tự trong các dòng mã đầu tiên của chúng thì chúng ta có một thiết kế xấu.
Tulains Córdova

7
@ user1598390: Câu hỏi không nằm ngoài chủ đề ở đây và không có gì về phạm vi của Lập trình viên khiến câu hỏi đặc biệt có ý nghĩa ở đó.
Robert Harvey

Câu trả lời:


90

Tôi không biết về thanh lịch, nhưng đây là một triển khai hoạt động bằng cách sử dụng sẵn Java java.lang.reflect.Proxyđể thực thi rằng tất cả các yêu cầu phương thức Foobắt đầu bằng cách kiểm tra enabledtrạng thái.

main phương pháp:

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 giao diện:

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 lớp học:

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);
        }
    }
}

Như những người khác đã chỉ ra, có vẻ như quá mức cho những gì bạn cần nếu bạn chỉ có một số phương pháp để lo lắng.

Điều đó nói rằng, chắc chắn có lợi ích:

  • Một sự tách biệt các mối quan tâm đã đạt được, bởi vì Fooviệc triển khai phương pháp không phải lo lắng vềenabled mối quan tâm xuyên suốt kiểm tra. Thay vào đó, mã của phương thức chỉ cần lo lắng về mục đích chính của phương thức là gì, không có gì nữa.
  • Không có cách nào để một nhà phát triển vô tội thêm một phương thức mới vào Foolớp và nhầm "quên" để thêm enabledkiểm tra. Cácenabled hành vi kiểm tra được tự động thừa hưởng bởi bất kỳ phương pháp mới được bổ sung.
  • Nếu bạn cần thêm một mối quan tâm xuyên suốt khác, hoặc nếu bạn cần tăng cường enabled kiểm tra, rất dễ dàng để thực hiện một cách an toàn và ở một nơi.
  • Thật tuyệt khi bạn có thể có hành vi giống AOP này với chức năng Java tích hợp. Bạn không bị buộc phải tích hợp một số khung công tác khác Spring, mặc dù chúng chắc chắn cũng có thể là những lựa chọn tốt.

Công bằng mà nói, một số nhược điểm là:

  • Một số mã thực thi xử lý các yêu cầu proxy là xấu. Một số người cũng nói rằng có các lớp bên trong để ngăn chặn việc khởi tạo FooImpllớp là xấu.
  • Nếu bạn muốn thêm một phương thức mới vào Foo, bạn phải thực hiện thay đổi ở 2 điểm: lớp triển khai và giao diện. Không phải là một vấn đề lớn, nhưng nó vẫn còn một chút công việc.
  • Yêu cầu proxy không miễn phí. Có một hiệu suất nhất định trên đầu. Đối với sử dụng chung, nó sẽ không được chú ý. Xem ở đây để biết thêm thông tin.

BIÊN TẬP:

Nhận xét của Fabian Streitel khiến tôi suy nghĩ về 2 điều phiền toái với giải pháp trên của tôi rằng, tôi sẽ thừa nhận, tôi không hài lòng về bản thân:

  1. Trình xử lý lời gọi sử dụng các chuỗi ma thuật để bỏ qua "kiểm tra kích hoạt" trên các phương thức "getEnables" và "setEnables". Điều này có thể dễ dàng phá vỡ nếu tên phương thức được tái cấu trúc.
  2. Nếu có một trường hợp cần thêm các phương thức mới mà không nên kế thừa hành vi "kiểm tra kích hoạt", thì nhà phát triển có thể dễ dàng hiểu sai điều này, và ít nhất, điều đó có nghĩa là thêm nhiều phép thuật dây.

Để giải quyết điểm # 1 và ít nhất là giải quyết vấn đề với điểm # 2, tôi sẽ tạo một chú thích BypassCheck(hoặc một cái gì đó tương tự) mà tôi có thể sử dụng để đánh dấu các phương thức trong Foogiao diện mà tôi không muốn thực hiện " kích hoạt kiểm tra ". Bằng cách này, tôi hoàn toàn không cần chuỗi ma thuật và nhà phát triển sẽ dễ dàng hơn rất nhiều để thêm một phương thức mới trong trường hợp đặc biệt này.

Sử dụng giải pháp chú thích, mã sẽ như thế này:

main phương pháp:

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 chú thích:

import java.lang.annotation.*;

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

Foo giao diện:

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 lớp học:

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
Tôi hiểu rằng đây là một giải pháp thông minh nhưng bạn có thực sự sử dụng nó không?
bhspencer

1
là một cách giải quyết tốt, bạn đang sử dụng mẫu proxy động để trang trí đối tượng với hành vi phổ biến này được tìm thấy ở đầu mỗi phương thức.
Victor

11
@bhspencer: Một câu hỏi rất chính đáng. Tôi thực sự đã sử dụng nó nhiều lần để xử lý ngoại lệ, ghi nhật ký, xử lý giao dịch, v.v. Tôi sẽ thừa nhận rằng đối với các lớp nhỏ hơn, nó có vẻ như quá mức cần thiết, và rất có thể là như vậy. Nhưng nếu tôi mong đợi lớp học sẽ phát triển rất nhiều về độ phức tạp và muốn đảm bảo hành vi nhất quán trên tất cả các phương pháp khi tôi làm điều đó, tôi không bận tâm đến giải pháp này.
sstan

1
Không phải là một phần của 97% tội ác ở đây, nhưng ý nghĩa hiệu suất của lớp proxy này là gì?
corsiKa

5
@corsiKa: Câu hỏi hay. Không có nghi ngờ rằng việc sử dụng proxy động chậm hơn so với các cuộc gọi phương thức trực tiếp. Đối với sử dụng chung, chi phí hiệu năng sẽ không được chấp nhận. Chủ đề SO liên quan nếu bạn quan tâm: Chi phí hiệu năng của proxy động Java
sct

51

Có rất nhiều gợi ý hay .. những gì bạn có thể làm để giải quyết vấn đề của mình là suy nghĩ trong Mô hình Nhà nước và thực hiện nó.

Hãy xem đoạn mã này .. có lẽ nó sẽ đưa bạn đến một ý tưởng. Trong kịch bản này, có vẻ như bạn muốn sửa đổi toàn bộ phương thức thực hiện dựa trên trạng thái bên trong của đối tượng. Vui lòng nhớ lại rằng tổng của các phương thức trong một đối tượng được biết là hành vi.

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() {}

      }
}

Hy vọng bạn thích nó!

PD: Là một triển khai của Mô hình Nhà nước (còn được gọi là Chiến lược tùy thuộc vào bối cảnh .. nhưng các nguyên tắc chỉ giống nhau).


1
OP muốn không phải lặp lại cùng một dòng mã ở đầu mỗi phương thức và giải pháp của bạn liên quan đến việc lặp lại cùng một dòng mã ở đầu mỗi phương thức.
Tulains Córdova

2
@ user1598390 không cần phải lặp lại sự đánh giá, bên trong FooEnablesBehaviour bạn đang giả sử rằng ứng dụng khách của đối tượng này đã đặt fooEnables thành true, do đó không cần phải cheking. Điều tương tự cũng xảy ra với lớp FooDisablesBehaviour. Kiểm tra lại, mã trong đó.
Victor

2
Cảm ơn @ bayou.io, chúng ta hãy đợi cho đến khi OP trả lời. Tôi nghĩ rằng cộng đồng đang làm một công việc kinh khủng ở đây, có rất nhiều lời khuyên tốt ở đây!
Victor

2
Đồng ý với @dyesdyes Tôi không thể tưởng tượng việc thực hiện điều này cho bất cứ điều gì ngoại trừ một lớp học thực sự tầm thường. Thật quá rắc rối, khi xem xét rằng bar()trong FooEnabledBehaviorbar()trong FooDisabledBehaviorcó thể chia sẻ rất nhiều mã giống nhau, thậm chí có thể có một dòng khác nhau giữa hai dòng. Bạn có thể rất dễ dàng, đặc biệt là nếu mã này được duy trì bởi các nhà phát triển cơ sở (chẳng hạn như bản thân tôi), kết thúc với một mớ hỗn độn khổng lồ không thể nhầm lẫn và không thể kiểm chứng. Điều đó có thể xảy ra với bất kỳ mã nào, nhưng điều này dường như rất dễ dàng để làm hỏng rất nhanh. +1 mặc dù, vì đề xuất tốt.
Chris Cirefice 01/07/2015

1
Mmmmm ... tôi không phải các bạn .. nhưng trước tiên, cảm ơn vì đã bình luận. Đối với tôi kích thước của mã không phải là vấn đề miễn là nó "sạch" và "có thể đọc được". Có lợi cho tôi, tôi nên lập luận rằng tôi không sử dụng bất kỳ lớp bên ngoài nào .. điều đó sẽ làm cho mọi thứ dễ tiếp cận hơn. Và nếu có một số hành vi phổ biến, hãy gói gọn nó trong CommonBehaviourClass và ủy thác đến nơi cần thiết. Trong cuốn sách GOF (không phải cuốn sách yêu thích của tôi để học, nhưng có công thức nấu ăn tốt) bạn đã tìm thấy ví dụ này: en.wikipedia.org/wiki/ . Là nhiều hay ít điều tương tự tôi làm ở đây.
Victor

14

Vâng, nhưng đó là một chút công việc, vì vậy nó phụ thuộc vào mức độ quan trọng của nó đối với bạn.

Bạn có thể định nghĩa lớp là một giao diện, viết một triển khai ủy nhiệm và sau đó sử dụng java.lang.reflect.Proxyđể thực hiện giao diện với các phương thức thực hiện phần được chia sẻ và sau đó gọi một cách có điều kiện.

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);

Bạn MyInvocationHandlercó thể trông giống như thế này (xử lý lỗi và bỏ qua giàn giáo lớp, giả sử fooIsEnabledđược xác định ở đâu đó có thể truy cập được):

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

Nó không đẹp một cách khó tin. Nhưng không giống như nhiều người bình luận khác, tôi sẽ làm điều đó, vì tôi nghĩ rằng việc lặp đi lặp lại là một rủi ro quan trọng hơn loại mật độ này và bạn sẽ có thể tạo ra "cảm giác" về lớp học thực sự của mình, với trình bao bọc có phần khó hiểu này được thêm vào rất cục bộ chỉ trong một vài dòng mã.

Xem tài liệu Java để biết chi tiết về các lớp proxy động.


14

Câu hỏi này liên quan chặt chẽ đến lập trình định hướng theo khía cạnh . AspectJ là một phần mở rộng AOP của Java và bạn có thể cung cấp cho nó một cái nhìn để có được một số kết quả.

Theo như tôi biết thì không có hỗ trợ trực tiếp cho AOP trong Java. Có một số mẫu GOF liên quan đến nó, ví dụ như Phương thứcChiến lược mẫu , nhưng nó sẽ không thực sự giúp bạn tiết kiệm các dòng mã.

Trong Java và hầu hết các ngôn ngữ khác, bạn có thể xác định logic lặp lại mà bạn cần trong các hàm và áp dụng cách tiếp cận mã hóa có kỷ luật mà bạn gọi chúng vào đúng thời điểm.

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

Tuy nhiên, điều này sẽ không phù hợp với trường hợp của bạn bởi vì bạn muốn mã được bao thanh toán có thể trở về checkBalance. Trong các ngôn ngữ hỗ trợ macro (như C / C ++), bạn có thể định nghĩa checkSomePreconditioncheckSomePostconditionlà macro và đơn giản chúng sẽ được thay thế bởi bộ tiền xử lý trước khi trình biên dịch thậm chí được gọi:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java không có cái này. Điều này có thể xúc phạm ai đó nhưng tôi đã sử dụng công cụ tạo mã và tạo mẫu tự động để tự động hóa các tác vụ mã hóa lặp đi lặp lại trong quá khứ. Nếu bạn xử lý các tệp Java của mình trước khi biên dịch chúng bằng một bộ tiền xử lý phù hợp, ví dụ Jinja2, bạn có thể làm điều gì đó tương tự như những gì có thể có trong C.

Cách tiếp cận thuần Java có thể

Nếu bạn đang tìm kiếm một giải pháp Java thuần túy, những gì bạn có thể tìm thấy có lẽ sẽ không ngắn gọn. Tuy nhiên, nó vẫn có thể tạo ra các phần phổ biến trong chương trình của bạn và tránh trùng lặp mã và lỗi. Bạn có thể làm một cái gì đó như thế này (đó là một kiểu mẫu chiến lược đầy mệt mỏi). Lưu ý rằng trong C # và Java 8 và trong các ngôn ngữ khác có chức năng dễ xử lý hơn một chút, cách tiếp cận này thực sự có thể trông rất hay.

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");
      }
    });
  }
}

Loại kịch bản trong thế giới thực

Bạn đang phát triển một lớp để gửi khung dữ liệu cho robot công nghiệp. Robot mất thời gian để hoàn thành một lệnh. Khi lệnh được hoàn thành, nó sẽ gửi lại cho bạn một khung điều khiển. Robot có thể bị hỏng nếu nhận được lệnh mới trong khi trước đó vẫn đang được thực thi. Chương trình của bạn sử dụng một DataLinklớp để gửi và nhận các khung đến và đi từ robot. Bạn cần bảo vệ quyền truy cập vào DataLinkví dụ.

Các cuộc gọi giao diện người dùng chủ đề RobotController.left, right, uphoặc downkhi người dùng nhấp vào nút, nhưng cũng kêu gọi BaseController.tickđều đặn, để chuyển tiếp lệnh bật lại tính đến tin DataLinkví dụ.

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
Không phải điều này chỉ đá cái lon xuống đường. Nghĩa là người bảo trì trong tương lai thường phải nhớ nếu (! FooIsEnables) trở lại; khi bắt đầu mọi chức năng và bây giờ họ cần nhớ bảo vệ (Mã mới {... khi bắt đầu mọi chức năng. Điều này giúp ích như thế nào?
bhspencer

Tôi muốn bạn phân tích và bối cảnh nền damix911 ... tôi sẽ xây dựng các thể hiện Mã mới vào thời gian biên dịch (sử dụng các thành viên tĩnh riêng) giả sử rằng mã sẽ không thay đổi theo thời gian và đổi tên thay đổi thành "exec If" chuyển thành đối số là Điều kiện (Giống như lớp Vị ngữ) và Mã. Nhưng đó là chặt hạ cá nhân và hương vị.
Victor

1
@bhspencer Có vẻ khá vụng về trong Java và trong hầu hết các trường hợp, chiến lược này thực sự là một sự áp đảo của mã đơn giản khác. Không có nhiều chương trình có thể được hưởng lợi từ một mô hình như vậy. Tuy nhiên, một điều thú vị là chúng tôi đã tạo ra một biểu tượng mới protect, dễ sử dụng và tài liệu hơn. Nếu bạn nói với người bảo trì trong tương lai rằng mã quan trọng cần được bảo vệ protect, bạn đã nói cho anh ta biết phải làm gì. Nếu quy tắc bảo vệ thay đổi, mã mới vẫn được bảo vệ. Đây chính xác là lý do đằng sau việc xác định các chức năng nhưng OP cần phải "trả lại return" những chức năng không thể làm được.
damix911

11

Tôi sẽ xem xét tái cấu trúc. Mẫu này phá vỡ rất nhiều mẫu DRY (Đừng lặp lại chính mình). Tôi tin rằng điều này phá vỡ trách nhiệm giai cấp này. Nhưng điều này phụ thuộc vào sự kiểm soát mã của bạn. Câu hỏi của bạn rất cởi mở - bạn đang gọi Fooví dụ ở đâu?

Tôi cho rằng bạn có mã như

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

có lẽ bạn nên gọi nó là một cái gì đó như thế này:

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

Và giữ nó sạch sẽ. Ví dụ: đăng nhập:

this.logger.debug(createResourceExpensiveDump())

a logger không tự hỏi mình , nếu gỡ lỗi được kích hoạt. Nó chỉ đăng nhập.

Thay vào đó, lớp gọi cần kiểm tra điều này:

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

Nếu đây là một thư viện và bạn không thể kiểm soát cuộc gọi của lớp này, hãy ném một IllegalStateExceptiongiải thích tại sao, nếu cuộc gọi này là bất hợp pháp và gây rắc rối.


6
Nó chắc chắn đơn giản và dễ dàng hơn trên mắt. Nhưng nếu mục tiêu của OP là đảm bảo rằng, khi các phương thức mới được thêm vào, logic không được kích hoạt sẽ không bao giờ được bỏ qua, thì việc tái cấu trúc này không giúp thực thi điều đó dễ dàng hơn.
17:30

4
Ngoài ra đối với ví dụ nhật ký của bạn, tôi sẽ nói điều này liên quan đến việc lặp lại nhiều hơn nữa - mỗi lần bạn muốn đăng nhập, bạn phải kiểm tra trình ghi nhật ký được bật. Tôi có xu hướng đăng nhập nhiều dòng hơn số lượng phương thức trên bất kỳ lớp nào ...
T. Kiley

4
Điều này phá vỡ tính mô-đun, bởi vì bây giờ người gọi phải biết điều gì đó về phần bên trong của foo (trong trường hợp này là liệu nó có phải là fooEnables hay không). Đây là một ví dụ kinh điển khi tuân theo các quy tắc thực hành tốt nhất sẽ không giải quyết được vấn đề vì các quy tắc xung đột. (Tôi vẫn hy vọng ai đó sẽ đưa ra câu trả lời "tại sao tôi không nghĩ về điều đó?"
Ian Goldby

2
Vâng, nó phụ thuộc rất nhiều vào bối cảnh như nó có ý nghĩa hay không.
Ian Goldby

3
Ghi nhật ký chính xác là ví dụ, trong đó tôi không muốn lặp lại mã của mình. Tôi chỉ muốn viết LOG.debug ("...."); - Và logger nên kiểm tra xem tôi có thực sự muốn gỡ lỗi không. - Một ví dụ khác là đóng cửa / dọn dẹp. - Nếu tôi sử dụng AutoClosable, tôi không muốn có ngoại lệ nếu nó đã bị đóng, nó sẽ không làm gì cả.
Falco

6

IMHO giải pháp hiệu quả nhất và thanh lịch nhất cho vấn đề này là có nhiều hơn một triển khai Foo, cùng với phương thức xuất xưởng để tạo một:

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
  }
}

Hoặc một biến thể:

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
    }
  };
}

Như sct chỉ ra, thậm chí tốt hơn là sử dụng một giao diện:

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
  }
}

Thích ứng điều này với phần còn lại của hoàn cảnh của bạn (bạn đang tạo Foo mới mỗi lần hoặc sử dụng lại cùng một ví dụ, v.v.)


1
Đây không giống như câu trả lời của Konrad. Tôi thích điều này, nhưng tôi nghĩ sẽ an toàn hơn nếu, thay vì sử dụng kế thừa lớp, bạn đã sử dụng một giao diện như những người khác đã đề xuất trong câu trả lời của họ. Lý do rất đơn giản: Nhà phát triển quá dễ dàng thêm phương thức vào Foovà quên thêm phiên bản không có phương thức trong lớp mở rộng, do đó bỏ qua hành vi mong muốn.
sstan

2
@sstan Bạn nói đúng, điều đó sẽ tốt hơn. Tôi đã làm như thế này để sửa đổi ví dụ ban đầu của kristina ít nhất có thể để tránh phiền nhiễu, nhưng điều này có liên quan. Tôi sẽ thêm đề nghị của bạn vào câu trả lời của tôi.
Pepijn Schmitz

1
Tôi nghĩ rằng bạn bỏ lỡ một điểm. Bạn xác định xem isFooEnabledtại thời điểm khởi tạo của Foo. Quá sớm. Trong mã gốc, điều này được thực hiện khi một phương thức được chạy. Giá trị của isFooEnabledcó thể thay đổi trong khi chờ đợi.
Nicolas Barbulesco 04/07/2015

1
@NicolasBarbulesco Bạn không biết rằng, fooIsEnables có thể là một hằng số. Hoặc nó có thể được sử dụng theo cách làm cho nó có hiệu quả không đổi. Hoặc nó có thể được đặt ở một vài nơi được xác định rõ ràng để có thể dễ dàng lấy một phiên bản mới của Foo mỗi lần. Hoặc có thể chấp nhận để có một phiên bản mới của Foo mỗi khi nó được sử dụng. Bạn không biết, đó là lý do tại sao tôi viết: "thích ứng điều này với phần còn lại của hoàn cảnh của bạn."
Pepijn Schmitz

@PepijnSchmitz - Tất nhiên, fooIsEnabled có thể là hằng số. Nhưng không có gì cho chúng ta biết rằng nó là hằng số. Vì vậy, tôi xem xét trường hợp chung.
Nicolas Barbulesco 04/07/2015

5

Tôi có một cách tiếp cận khác: có một

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() {}
}

}

và sau đó bạn có thể làm

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

Có lẽ bạn thậm chí có thể thay thế isFooEnabledbằng một Foobiến giữ hoặc FooImplđược sử dụng hoặc NullFoo.DEFAULT. Sau đó, cuộc gọi lại đơn giản hơn:

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

BTW, đây được gọi là "mẫu Null".


Cách tiếp cận quá mức là tốt, nhưng sử dụng biểu thức (isFooEnabled ? foo : NullFoo.DEFAULT).bar();có vẻ hơi vụng về. Có một triển khai thứ ba mà đại biểu cho một trong những triển khai hiện có. Thay vì thay đổi giá trị của một lĩnh vực isFooEnabled, mục tiêu của phái đoàn có thể được thay đổi. Điều này làm giảm số lượng chi nhánh trong mã
SpaceTrucker

1
Nhưng bạn đang trục xuất việc nấu ăn nội bộ của lớp Footrong mã gọi! Làm thế nào chúng ta có thể biết liệu isFooEnabled? Đây là một lĩnh vực nội bộ trong lớp Foo.
Nicolas Barbulesco

3

Theo cách tiếp cận chức năng tương tự như câu trả lời của @ Colin, với các hàm lambda của Java 8 , có thể gói tính năng có điều kiện để chuyển mã bật / tắt mã thành phương thức bảo vệ ( executeIfEnabled) chấp nhận hành động lambda, mã nào được thực thi theo điều kiện thông qua.

Mặc dù trong trường hợp của bạn, cách tiếp cận này sẽ không lưu bất kỳ dòng mã nào, bằng cách DRYing lên, giờ đây bạn có tùy chọn tập trung vào tính năng khác để chuyển các mối quan tâm, cộng với AOP hoặc gỡ lỗi các mối quan tâm như ghi nhật ký, chẩn đoán, lược tả et al.

Một lợi ích của việc sử dụng lambdas ở đây là có thể sử dụng các bao đóng để tránh sự quá tải của executeIfEnabledphương thức.

Ví dụ:

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);
        });
    }

Với một bài kiểm tra:

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();
}

Cũng tương tự như cách tiếp cận của Damix, không cần giao diện và triển khai lớp ẩn danh với ghi đè phương thức.
StuartLC

2

Như được chỉ ra trong các câu trả lời khác, Mẫu thiết kế chiến lược là một mẫu thiết kế phù hợp để tuân theo để đơn giản hóa mã này. Tôi đã minh họa nó ở đây bằng cách sử dụng phương thức gọi thông qua sự phản chiếu, nhưng có bất kỳ số lượng cơ chế nào bạn có thể sử dụng để có được hiệu quả tương tự.

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...
  }
}

Phải đối phó với sự phản chiếu là một chút khó xử, nếu đã có các lớp làm điều này cho bạn, như đã được đề cập Proxy.
SpaceTrucker

Làm thế nào bạn có thể làm foo.fooIsEnabled ...? Một tiên nghiệm, đây là một lĩnh vực nội bộ của đối tượng, chúng ta không thể, và không muốn, nhìn thấy nó bên ngoài.
Nicolas Barbulesco 04/07/2015

2

Nếu chỉ có java thì tốt hơn một chút về chức năng. Nó nghĩ rằng giải pháp OOo nhất là tạo lớp bao bọc một hàm duy nhất để nó chỉ được gọi khi foo được bật.

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();
}

và sau đó thực hiện bar, bazbatnhư các lớp ẩn danh mở rộng 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
        }
    };
}

Một ý tưởng khác

Sử dụng giải pháp nullable của glglgl nhưng tạo FooImplNullFoocác lớp bên trong (với các hàm tạo riêng) của lớp bên dưới:

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;
    }  
}

Bằng cách này, bạn không phải lo lắng về việc nhớ sử dụng (isFooEnabled ? foo : NullFoo.DEFAULT).


Nói rằng bạn có: Foo foo = new Foo()để gọi barbạn sẽ viếtfoo.bar.call()
Colin

1

Có vẻ như lớp không làm gì khi Foo không được kích hoạt, vậy tại sao không thể hiện điều này ở mức cao hơn nơi bạn tạo hoặc lấy ví dụ Foo?

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

Điều này chỉ hoạt động nếu isFooEnables là một hằng số. Trong trường hợp chung, bạn có thể tạo chú thích của riêng bạn.


Konrad, bạn có thể phát triển trên chú thích?
Nicolas Barbulesco 04/07/2015

Mã ban đầu xác định fooIsEnabledkhi một phương thức được gọi. Bạn làm điều này trước khi khởi tạo Foo. Quá sớm. Giá trị có thể thay đổi trong khi chờ đợi.
Nicolas Barbulesco 04/07/2015

Tôi nghĩ rằng bạn bỏ lỡ một điểm. Một tiên nghiệm, isFooEnabledlà một trường Foođối tượng.
Nicolas Barbulesco 04/07/2015

1

Tôi không quen với cú pháp Java. Giả định rằng trong Java, có tính đa hình, thuộc tính tĩnh, lớp & phương thức trừu tượng:

    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
        }
    }

Ai nói rằng được kích hoạt là thuộc tính tĩnh của lớp?
Nicolas Barbulesco 04/07/2015

new bar()nghĩa là gì?
Nicolas Barbulesco 04/07/2015

Trong Java, chúng tôi viết một lớp Namevới một chữ in hoa.
Nicolas Barbulesco 04/07/2015

Điều này cần thay đổi cách gọi mã quá nhiều. Chúng tôi gọi một phương thức bình thường : bar(). Nếu bạn cần thay đổi điều đó, bạn sẽ phải chịu số phận.
Nicolas Barbulesco 04/07/2015

1

Về cơ bản, bạn có một cờ mà nếu được đặt, nên bỏ qua lệnh gọi hàm. Vì vậy, tôi nghĩ rằng giải pháp của tôi sẽ là ngớ ngẩn, nhưng đây là.

Foo foo = new Foo();

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

Đây là việc triển khai một Proxy đơn giản, trong trường hợp bạn muốn thực thi một số mã trước khi thực hiện bất kỳ chức năng nào.

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 ()
    {
        // ..
    }
}

Đối với khối mã đầu tiên của bạn, bạn cần một phương thức hiển thị isEnabled(). Một tiên nghiệm, được kích hoạt là nấu ăn nội bộ Foo, không tiếp xúc.
Nicolas Barbulesco 04/07/2015

Mã cuộc gọi không thể , và không muốn , biết liệu đối tượng có được bật hay không .
Nicolas Barbulesco 04/07/2015

0

Có một giải pháp khác, sử dụng ủy nhiệm (con trỏ tới hàm). Bạn có thể có một phương thức duy nhất trước tiên là thực hiện xác nhận và sau đó gọi đến phương thức có liên quan theo hàm (tham số) sẽ được gọi. Mã 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
Chính xác, nó được đánh dấu Java và đây là lý do tại sao tôi nhấn mạnh và viết "Mã trong C #" vì tôi không biết Java. Vì nó là một câu hỏi Mẫu thiết kế nên ngôn ngữ không quan trọng.
ehh

Oh! Tôi hiểu, xin lỗi vì điều đó, chỉ cố gắng giúp đỡ và tìm ra giải pháp. Cảm ơn bạn
ehh
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.