모든 방법이 동일한 방식으로 시작되는 클래스가 있습니다.
class Foo {
public void bar() {
if (!fooIsEnabled) return;
//...
}
public void baz() {
if (!fooIsEnabled) return;
//...
}
public void bat() {
if (!fooIsEnabled) return;
//...
}
}
fooIsEnabled
수업의 모든 공개 방법에 대한 부분 을 요구하고 (매번 작성하지 않기를 바랍니다) 좋은 방법이 있습니까?
답변
나는 우아한 것에 대해 잘 모르지만 여기에 모든 메소드 호출 이 상태 를 확인하여 시작 java.lang.reflect.Proxy
하도록 강제 하는 Java 내장 기능을 사용하여 작동하는 구현이 있습니다.Foo
enabled
main
방법:
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
상호 작용:
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
수업:
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);
}
}
}
다른 사람들이 지적했듯이, 걱정할 방법이 몇 가지만 있다면 필요한 것을 과도하게 사용하는 것처럼 보입니다.
즉, 확실히 이점이 있습니다.
Foo
의 메소드 구현은enabled
교차 검사 문제에 대해 걱정할 필요가 없기 때문에 우려의 특정 분리가 달성 됩니다. 대신, 메소드의 코드는 메소드의 주요 목적이 무엇인지에 대해서만 걱정하면됩니다.- 무고한 개발자가
Foo
클래스에 새 메소드를 추가 하고 실수로enabled
수표 를 추가하는 것을 “잊어 버린” 방법은 없습니다 .enabled
체크 동작은 자동으로 새로 추가 한 방법으로 상속됩니다. - 다른 교차 절단 문제를 추가해야하거나
enabled
점검 을 강화해야하는 경우 안전하고 한 곳에서 쉽게 수행 할 수 있습니다. - 내장 된 Java 기능으로이 AOP와 유사한 동작을 얻을 수 있다는 것은 좋은 일입니다. 와 같은 다른 프레임 워크를 통합하지 않아도
Spring
되지만 좋은 옵션 일 수도 있습니다.
공정하게하기 위해 몇 가지 단점은 다음과 같습니다.
- 프록시 호출을 처리하는 일부 구현 코드는보기 흉하다. 일부는 클래스의 인스턴스화를 막기 위해 내부 클래스를 갖는
FooImpl
것이 추악 하다고 말합니다 . - 에 새로운 메소드를 추가
Foo
하려면 구현 클래스와 인터페이스의 두 지점을 변경해야합니다. 큰 문제는 아니지만 여전히 조금 더 많은 작업입니다. - 프록시 호출은 무료가 아닙니다. 특정 성능 오버 헤드가 있습니다. 그러나 일반적인 용도로는 눈에 띄지 않습니다. 자세한 내용은 여기 를 참조하십시오.
편집하다:
Fabian Streitel의 의견은 위의 해결책으로 2 가지 성가심에 대해 생각하게 만들었습니다.
- 호출 핸들러는 매직 문자열을 사용하여 “getEnabled”및 “setEnabled”메소드에서 “enabled-check”를 건너 뜁니다. 메소드 이름이 리팩터링되면 쉽게 중단 될 수 있습니다.
- “활성화 된 검사”동작을 상속하지 않아야하는 새로운 방법을 추가해야하는 경우 개발자가이 문제를 쉽게 파악할 수 있으며 최소한 마법을 더 추가하는 것을 의미합니다. 문자열.
1 번 지점을 해결하고 2 번 지점의 문제를 해결하기 위해 인터페이스 BypassCheck
에서 메소드를 표시하고 Foo
싶지 않은 주석 (또는 유사한 항목)을 ” 가능 확인 “. 이런 식으로, 나는 마술 문자열이 전혀 필요하지 않으며, 개발자 가이 특별한 경우에 새로운 방법을 올바르게 추가하는 것이 훨씬 쉬워집니다.
주석 솔루션을 사용하면 코드는 다음과 같습니다.
main
방법:
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
주석:
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BypassCheck {
}
Foo
상호 작용:
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
수업:
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);
}
}
}
답변
좋은 제안이 많이 있습니다. 문제를 해결하기 위해 할 수있는 일은 상태 패턴에서 생각하고 구현하는 것입니다.
이 코드 스 니펫을 살펴보십시오. 아마도 아이디어를 얻을 수 있습니다. 이 시나리오에서는 객체의 내부 상태를 기반으로 전체 메소드 구현을 수정하려는 것으로 보입니다. 객체의 메소드 합계는 동작이라고합니다.
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() {}
}
}
네가 좋아하길 바래!
PD : 상태 패턴을 구현 한 것입니다 (상황에 따라 전략이라고도합니다. 그러나 원리는 동일합니다).
답변
예,하지만 약간의 일이므로, 그것이 당신에게 얼마나 중요한지에 달려 있습니다.
클래스를 인터페이스로 정의하고 대리자 구현을 작성한 다음 java.lang.reflect.Proxy
공유 부분을 수행하고 조건부 대리자를 호출하는 메소드로 인터페이스를 구현하는 데 사용할 수 있습니다.
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);
귀하의 MyInvocationHandler
이 같은 캔 모양의 무언가 (생략 오류 처리 및 클래스 비계는 가정하에 fooIsEnabled
곳 접근 정의) :
public Object invoke(Object proxy, Method method, Object[] args) {
if (!fooIsEnabled) return null;
return method.invoke(underlying, args);
}
엄청 예쁘지 않습니다. 그러나 다른 논평자와는 달리 반복은 이러한 종류의 밀도보다 더 중요한 위험이라고 생각하므로 다소 어설픈 래퍼를 추가하여 실제 클래스의 “느낌”을 만들 수 있습니다. 몇 줄의 코드로 매우 로컬에 있습니다.
동적 프록시 클래스에 대한 자세한 내용은 Java 설명서 를 참조하십시오 .
답변
이 질문은 측면 지향 프로그래밍 과 밀접한 관련이 있습니다. AspectJ는 Java의 AOP 확장이며, 약간의 영감을 얻을 수 있습니다.
내가 아는 한 Java에서는 AOP를 직접 지원하지 않습니다. 템플릿 방법 및 전략 과 같이 관련 GOF 패턴이 있지만 실제로 코드 줄을 저장하지는 않습니다.
Java 및 대부분의 다른 언어에서는 함수에 필요한 반복 논리를 정의하고 적절한 시간에 호출하는 소위 규칙 화 된 코딩 방식을 채택 할 수 있습니다.
public void checkBalance() {
checkSomePrecondition();
...
checkSomePostcondition();
}
그러나 인수 분해 된 코드가에서 반환되기를 원하기 때문에 이것은 귀하의 경우에 맞지 않습니다 checkBalance
. 매크로를 지원하는 언어 (C / C ++ checkSomePrecondition
와 checkSomePostcondition
같은)에서 매크로를 정의 하고 매크로로 사용할 수 있으며 컴파일러가 호출되기 전에 전처리기로 대체됩니다.
#define checkSomePrecondition \
if (!fooIsEnabled) return;
Java는 이것을 즉시 사용할 수 없습니다. 이것은 누군가를 화나게 할 수 있지만 자동 코드 생성 및 템플릿 엔진을 사용하여 과거의 반복적 인 코딩 작업을 자동화했습니다. 적합한 프리 프로세서 (예 : Jinja2)로 컴파일하기 전에 Java 파일을 처리하는 경우 C에서 가능한 것과 유사한 작업을 수행 할 수 있습니다.
가능한 순수한 자바 접근
순수한 Java 솔루션을 찾고 있다면 간결하지 않을 것입니다. 그러나 여전히 프로그램의 공통 부분을 제외하고 코드 복제 및 버그를 피할 수 있습니다. 이런 식으로 할 수 있습니다 ( 전략- 영감을 얻은 패턴입니다). C # 및 Java 8 및 함수를 처리하기가 더 쉬운 다른 언어에서는이 방법이 실제로 멋지게 보일 수 있습니다.
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");
}
});
}
}
실제 시나리오의 종류
산업용 로봇에 데이터 프레임을 전송하는 클래스를 개발 중입니다. 로봇이 명령을 완료하는 데 시간이 걸립니다. 명령이 완료되면 제어 프레임을 다시 보냅니다. 이전 명령이 아직 실행되는 동안 새 명령을 받으면 로봇이 손상 될 수 있습니다. 프로그램은 DataLink
클래스를 사용 하여 로봇과 프레임을주고받습니다. DataLink
인스턴스에 대한 액세스를 보호해야 합니다.
사용자 인터페이스 스레드 호출 RobotController.left
, right
, up
또는 down
사용자는 버튼을 클릭 할뿐만 아니라, 통화시 BaseController.tick
전용 명령을 재실행 전달하기 위해, 정기적으로 DataLink
예.
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 */);
}});
}
}
답변
리팩토링을 고려할 것입니다. 이 패턴은 DRY 패턴을 크게 깨뜨리고 있습니다 (반복하지 마십시오). 나는 이것이이 계급의 책임을 어 기고 있다고 믿는다. 그러나 이것은 코드 제어에 달려 있습니다. 귀하의 질문은 매우 개방적입니다-어디에서 Foo
인스턴스 를 호출 합니까?
나는 당신이 같은 코드를 가지고 있다고 가정
foo.bar(); // does nothing if !fooEnabled
foo.baz(); // does also nothing
foo.bat(); // also
어쩌면 다음과 같이 호출해야 할 수도 있습니다.
if (fooEnabled) {
foo.bat();
foo.baz();
...
}
그리고 깨끗하게 유지하십시오. 예를 들어, 로깅 :
this.logger.debug(createResourceExpensiveDump())
디버그가 활성화 된 경우 a logger
는 자신에게 묻지 않습니다 . 그냥 기록합니다.
대신, 호출 클래스는 이것을 확인해야합니다.
if (this.logger.isDebugEnabled()) {
this.logger.debug(createResourceExpensiveDump())
}
이것이 라이브러리이고이 클래스의 호출을 제어 할 수없는 IllegalStateException
경우이 호출이 불법이고 문제를 일으키는 이유를 설명 하는를 던지십시오 .
답변
이를위한 가장 우아하고 성능이 뛰어난 IMHO 솔루션은 하나 이상의 Foo 구현 및 하나의 팩토리 메소드를 작성하는 것입니다.
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
}
}
또는 변형 :
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
}
};
}
sstan이 지적했듯이 인터페이스를 사용하는 것이 더 좋습니다.
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
}
}
이 상황을 나머지 상황에 맞게 조정하십시오 (매번 새 Foo를 생성하거나 동일한 인스턴스를 재사용하는 등).
답변
다른 접근법이 있습니다.
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() {}
}
}
그리고 당신은 할 수 있습니다
(isFooEnabled ? foo : NullFoo.DEFAULT).bar();
어쩌면 사용할 또는를 보유하고 isFooEnabled
있는 Foo
변수로를 대체 할 수도 FooImpl
있습니다 NullFoo.DEFAULT
. 그런 다음 통화가 다시 간단 해집니다.
Foo toBeUsed = isFooEnabled ? foo : NullFoo.DEFAULT;
toBeUsed.bar();
toBeUsed.baz();
toBeUsed.bat();
BTW, 이것을 “널 패턴”이라고합니다.