지금 개발 중인 프로그램과 관련해서 도움이 될만한 글이 있어 실습해보았다.
- UI로 부터 독립적인 클래스라이브러리(Viewmodel) 개발
- UI없이 독립적으로 UnitTest가 가능
Sourecode
https://github.com/paranpiano/CrossPlattformLib.git
지금 개발 중인 프로그램과 관련해서 도움이 될만한 글이 있어 실습해보았다.
- UI로 부터 독립적인 클래스라이브러리(Viewmodel) 개발
- UI없이 독립적으로 UnitTest가 가능
Sourecode
https://github.com/paranpiano/CrossPlattformLib.git
MVVM Pattern으로 개발을 다시 하기 전에 개념을 정리하고 싶었다. :)
Winform , Behind code
- View로 요청을 받음
- View의 control을 Behind code에서 바로 binding
- Model을 사용해서 business logic을 처리하더라도 View와 강하게 결합
Model - View(n) - Controller(1)
- Contoller로 요청을 받음
- Contoleer는 Model에게 Bisiness logic 처리 요청
- view는 모델을 참조하여 업데이트 함,
- View와 model과 의존성이 없앨수 없다.
- Model과 view의 의존성을 낮게하는 패턴을 사용하면 좋은 MVC가 된다
Model - view(1) - Presenter(1)
- View로 요청을 받음
- Presenter는 Model에게 Bisiness logic 처리 요청
- 그 결과는 View에게 알려준다.
- Model와 View과 완벽하게 분리
- View와 Presenter의 의존성이 강하다.(1:1결합)
Model - view(N) - Viewmodel(1)
- View로 요청을 받음
- Command : View의 Behavior를 Command로 View model에게 요청
- View Model은 Model에게 Bisness log을 요청
- Data binding : View Model의 속성 값이 변경되면 binding된 View control에게 notify가 가면서 자동 업데이트
- Model은 View로 부터 완벽히 분리
- MVP와 달리 Viewmodel은 여러 View에서 재사용이 가능하다.
Strategy Pattern을 어떻게 대안이 될 수 있는가? ^^
- Factory Method pattern Diagram
class FLEX :Equipment
{
public override void Start()
{
//VK93과 같은 일을 한다(중복코드 발생)
//do someting V93K about start
}
public override void Load()
{
//CATALIST와 같은 일을 한다(중복코드 발생)
//do someting CATALIST about load
}
public override void Modify()
{
//do someting FLEX about modify
}
}
문제점 : VK93의 Start() or Catalist 의 Load() 함수가 변경된다면 같이 수정이 필요
class D10 : Equipment
{
public override void Start()
{
//do someting D10 about load
}
public override void Load()
{
//do someting D10 about load
}
//LSP(서브타입은 언제나 자신이 기반타입으로 교체할 수 있어야 한다) 위반
public override void Modify()
{
throw new NotImplementedException();
}
}
public partial class Form1 : Form
{
EquipmentCommander _ec = new EquipmentCommander();
public Form1()
{
InitializeComponent();
Equipment vk93 = EquipmentFactory.CreateEquipment("V93K");
Equipment Catalist = EquipmentFactory.CreateEquipment("Catalist");
Equipment D10 = EquipmentFactory.CreateEquipment("D10");
Equipment Flex = EquipmentFactory.CreateEquipment("FLEX");
_ec.Excute(vk93);
_ec.Excute(Catalist);
_ec.Excute(Flex);
//서브클래스 인스턴스를 파라미터로 전달 했을 때 메소드가 이상하게 작동
//Modify function이 제대로 구현되지 않음
_ec.Excute(D10);
}
}
문제점 : LSP를 위반한다. 즉 기반 클래스로 서브클래스의 사용을 하지 못한다.
- Strategy Pattern Diagram
public class Equipment
{
public int ID { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public string PropertyforManager { get; set; }
//각각 Strategy 를 가지고 있다.
public IStart start { get; set; }
public IModify modify { get; set; }
public ILoad load { get; set; }
}
Factory 에서 생성 시에 맞는 전략을 구현한다.
class EquipmentFactory
{
public static Equipment CreateEquipment(string EqpName)
{
Equipment eqp = new Equipment();
eqp.Name = EqpName;
if (EqpName.Equals("V93K"))
{
eqp.start = new StartV93K();
eqp.load = new LoadV93K();
eqp.modify = new ModifyV93K();
}
else if (EqpName.Equals("Catalist"))
{
eqp.start = new StartCatalist();
eqp.load = new LoadCatalist();
eqp.modify = new ModifyCatalist();
}
else if (EqpName.Equals("D10"))
{
eqp.start = new StartD10();
eqp.load = new LoadD10 ();
//modify 가 필요없는 Equipment는 ModifyNothing을 구현한다.
eqp.modify = new ModifyNothing();
}
else if (EqpName.Equals("FLEX"))
{
// Flex는 V93의 Start와 Catalist의 Load를 구현함으로 중복 코드의 없음
eqp.start = new StartV93K();
eqp.load = new LoadCatalist();
eqp.modify = new ModifyFlex();
}
else
{
throw new Exception("unregisted Eqp Class");
}
return eqp;
}
}
문제점 해결 : 전략 객체를 이용해서 장비가 행위를 한다.
중복코드를 만들지 않는다.(수정이 필요하면 전략객체만을 수정하면 된다.)
새로운 행위가 필요하면 기반 클래스의 수정없이 전략 클래스를 확장(OCP를 지킴)
public partial class Form1 : Form
{
EquipmentCommander _ec = new EquipmentCommander();
public Form1()
{
InitializeComponent();
Equipment vk93 = EquipmentFactory.CreateEquipment("V93K");
Equipment Catalist = EquipmentFactory.CreateEquipment("Catalist");
Equipment D10 = EquipmentFactory.CreateEquipment("D10");
Equipment Flex = EquipmentFactory.CreateEquipment("FLEX");
_ec.Excute(vk93);
_ec.Excute(Catalist);
_ec.Excute(Flex);
//D10은 modify를 하지 않지만 ModifyNothing()을 구현해서 문제를 해결한다.
=> Equipment Generator Code
else if (EqpName.Equals("D10"))
{
eqp.start = new StartD10();
eqp.load = new LoadD10 ();
_ec.Excute(D10);
//Flex장비의 행동을vk93과 같게 변경하고 싶다면??
//클래스를 생성해서 위임해야 겠지만 예제 이므로 여기서 테스트 하였음
Flex.start = new StartV93K();
Flex.load = new LoadV93K();
Flex.modify = new ModifyV93K();
_ec.Excute(Flex);
}
}
문제점 해결 : EquipmentCommander 의 Execute가 기반 타입으로 작동하는데 문제가 없다
즉 LSP를 위반하지 않는다 ^^
기본 아이디어
- Adapter 패턴은 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변화(어댑팅)한다. 이는 호환성 없는 인터페이스 때문에 함께 사용할 수 없는 클래스를 개조하여 함께 작동하도록 해준다.
// ResultSet interface를 구현한 ResultSetAdapter
public class ResultSetAdapter implements java.sql.ResultSet
{
public ResultSetAdapter() {}
public boolean next() throws SQLException {throw new SQLException("ResultSet.next() unsupported");}
public int findColumn(String columnName)throws SQLException
{throw new SQLException("ResultSet.findColumn(String columnName) unsupported");}
public void updateObject(String colIndex, Object obj) throws SQLException {throw new SQLException("ResultSet.updateObject(String colIndex, Object obj) unsupported");}
public void updateObject(String colIndex, Object obj, int s) throws SQLException {throw new SQLException("ResultSet.updateObject(String colIndex, Object obj, int s) unsupported");}
public Statement getStatement() throws SQLException {throw new SQLException("ResultSet.getStatement() unsupported");}
public void close() throws SQLException {throw new SQLException("ResultSet.close() unsupported");}
public void checkClosed() throws SQLException {throw new SQLException("ResultSet.checkClosed() unsupported");}
}
// ResultSetAdapter를 상속받은 JDBCResultSet
public class JDBCResultSet extends ResultSetAdapter
{
private final Cursor cursor;
private static final NumberFormat format =
NumberFormat.getInstance();
/** Wrap a result set around a Cursor. The cursor
* should never have been advanced; just pass this constructor
* the return value from {@link Table#rows}.
*/
public JDBCResultSet(Cursor cursor) throws SQLException
{
this.cursor = cursor;
}
public boolean next()
{ return cursor.advance();
}
public String getString(String columnName) throws SQLException
{ try
{ Object contents = cursor.column(columnName);
return (contents==null) ? null : contents.toString();
}
catch( IndexOutOfBoundsException e )
{ throw new SQLException("column "+columnName+" doesn't exist" );
}
}
public double getDouble(String columnName) throws SQLException
{ try
{ String contents = getString(columnName);
return (contents == null)
? 0.0
: format.parse( contents ).doubleValue()
;
}
catch( ParseException e )
{ throw new SQLException("field doesn't contain a number");
}
}
public int getInt(String columnName) throws SQLException
{ try
{ String contents = getString(columnName);
return (contents == null)
? 0
: format.parse( contents ).intValue()
;
}
catch( ParseException e )
{ throw new SQLException("field doesn't contain a number");
}
}
public long getLong(String columnName) throws SQLException
{ try
{ String contents = getString(columnName);
return (contents == null)
? 0L
: format.parse( contents ).longValue()
;
}
catch( ParseException e )
{ throw new SQLException("field doesn't contain a number");
}
}
public void updateNull(String columnName )
{ cursor.update(columnName, null );
}
public void updateDouble(String columnName, double value)
{ cursor.update(columnName, format.format(value) );
}
public void updateInt(String columnName, long value)
{ cursor.update(columnName, format.format(value) );
}
public ResultSetMetaData getMetaData() throws SQLException
{ return new JDBCResultSetMetaData(cursor);
}
}
- Coursor 와 java.sql.ResultSet는 적어도 핵심관점에서는 차이가 없다, 또한 JDBCResultSet class는 Cursor 객채를 래핑하고 있기 때문에 Coursor 객체가 인터페이스를 구현하고 있는 것처럼 보일 수도 있다. 하지만 JDBCResultSet은 Cursor(Adaptee)가 Taget 인터페이스(java.sql.ResultSet)를 구현하고 있는 것처럼 보이도록 하는 Adapter이다.
public final class ArrayIterator implements Iterator
{
private int position = 0;
private final Object[] items;
public ArrayIterator(Object[] items){ this.items = items; }
public boolean hasNext()
{
return ( position < items.length );
}
public Object next()
{
if( position >= items.length )
throw new NoSuchElementException();
return items[ position++ ];
}
public void remove()
{
throw new UnsupportedOperationException(
"ArrayIterator.remove()");
}
/** Not part of the Iterator interface, returns the data
* set in array form. Modifying the returned array will
* not affect the iteration at all.
*/
public Object[] toArray()
{
return (Object[]) items.clone();
}
}
Arrayiterator 클래스(Adapter)는 배열(Adaptee)이 Iterator 인터페이스를 구현하고 있는 것 처럼 만들어 주기 때문에 배열을 Iterator를 통해 접근할 수 있게 해준다.
Bridge 패턴 VS Adapter 패턴
Bridge : 서브시스템들을 분리시키는 거 , 대규모
Adapter: 어떤 클래스를 이 클래스가 구현하고 있지 않은 인터페이스를 사용하여 작성한 프로그램에 적용시키기 위한 것, 소규모
Decorator 패턴 VS Adapter 패턴
Decorator : 어떤 클래스의 메소드를 행위를 상속(Extends)을 하지 않고 변경시키는 것
구조 측면에서 보자면 항상 자신이 데코레이팅하는 객체와 같은 인터페이스를 구현한다.
Adapter : 대부분의 경우 래핑되는 객체의 인터페이스를 구현하지 않는다.
- Command pattern과 비슷하다는 생각이 들어서 찾아본 검색 결과 후 답변
The Command
design pattern is used to solve problems like:
- How can an object be configured (customized) with a request?
- And how can the request be (ex)changed dynamically at run-time?
The point of Command
is to decouple a request from its invoker and encapsulate it in a separate object (Command interface).
Invoker then delegates a request to a command object dynamically.
The Adapter
design pattern (object adapter) is used to solve problems like:
- How can an object be accessed that has an incompatible interface
without changing existing interfaces?
The point of Adapter
is to work through a separate object that adapts an incompatible interface, i.e., that implements our needed interface (Target) in terms of (by delegating to) the incompatible interface.
The Command
pattern is more similar to the Strategy
pattern, which decouples an algorithm from its context and encapsulates it in a separate object (Strategy).
- Can we say the two patterns have the same implementation but different aims ? Maybe the implementation of the Adapter (object adapter) is somewhat easier: The Adapter pattern implements an existing interface (Target). Clients refer to this interface and needn't to be changed. The Command pattern defines a new interface (Command) and implements different requests. Clients must be designed/implemented/changed so that they delegate a request to one of different command objects. |
기본 아이디어
State 패턴은 객체의 내부 상태에 따라 행위를 변경할 수 있도록 해준다. 이렇게 하면 객체는 마치 클래스를 바꾸는 것처럼 보인다.
- 오토 커밋과 같은 행위를 Gof의 State 패턴을 사용하여 구현, State 패턴은 상태가 바뀔 때 행위가 바뀌게 되는 객체들을 조직화할 수 있는 방법을 제공한다. 아래 예제에서는 내 개 메소드(close,commit, rollback, setAutoCommit) 행위가 커넥션이 오토 커밋되어 있는지 아닌지 따라 바뀌게 된다.
상태에 따라 변화하는 행위를 구현하는 가장 단순한 방법은 현재 상태를 나타내는 필드를 두고, 각 메소드에서 필드 값을 처리하는 switch 문을 두어 case 문이 현재 상태화 관련된 행위를 제공하는 것이다.
class ChangesBehaviorWithState
{
private static int state = 0;
public void methodOne()
{
switch( state )
{
case 0: /* behavior for state 0 goes here */ break;
case 1: /* behavior for state 1 goes here */ break;
case 2: /* behavior for state 2 goes here */ break;
}
}
public void methodTwo()
{
switch( state )
{
case 0: /* behavior for state 0 goes here */ break;
case 1: /* behavior for state 1 goes here */ break;
case 2: /* behavior for state 2 goes here */ break;
}
}
//...
}
위 접근 방법은 2가지 문제점이 있다.
1. Swich 문은 코드를 읽기 어렵게 만든다
2. 특정 상태와 연관되어 있는 해위를 제공하는 부분이 코드 전반에 흩어져 있기 때문에 유지보수가 어렵게 된다.
그러므로 내부의 상태머신(어떻게 한 상태에서 다른 상태로 바뀌는지, 그리고 각상태와 연관된 행위를 결정하는 룰)을 바꾸는 것이 굉장히 어렵게 된다.
=> State 패턴으로 이 문제를 해결하자.
//State Pattern을 사용한 JDBCconnection 객체
public class JDBCConnection extends ConnectionAdapter
{
private Database database;
public JDBCConnection(String uri) throws SQLException,
URISyntaxException,
IOException
{
this( new URI(uri) );
}
public JDBCConnection(URI uri) throws SQLException,
IOException
{
database = new Database( uri );
}
//State Interface 와 Concrete State 객체 생성
private interface AutoCommitBehavior
{
void close() throws SQLException;
void commit() throws SQLException;
void rollback() throws SQLException;
void setAutoCommit( boolean enable ) throws SQLException;
}
private AutoCommitBehavior enabled =
new AutoCommitBehavior()
{
public void close() throws SQLException {/* nothing to do */}
public void commit() {/* nothing to do */}
public void rollback() {/* nothing to do */}
public void setAutoCommit( boolean enable )
{ if( enable == false )
{ database.begin();
autoCommitState = disabled;
}
}
};
private AutoCommitBehavior disabled =
new AutoCommitBehavior()
{
public void close() throws SQLException
{ try
{ database.commit();
}
catch( ParseFailure e )
{ throw new SQLException( e.getMessage() );
}
}
public void commit() throws SQLException
{ try
{ database.commit();
database.begin();
}
catch( ParseFailure e )
{ throw new SQLException( e.getMessage() );
}
}
public void rollback() throws SQLException
{ try
{ database.rollback();
database.begin();
}
catch( ParseFailure e )
{ throw new SQLException( e.getMessage() );
}
}
public void setAutoCommit( boolean enable ) throws SQLException
{ try
{ if( enable == true )
{ database.commit();
autoCommitState = enabled;
}
}
catch( ParseFailure e )
{ throw new SQLException( e.getMessage() );
}
}
};
//AutoCommitState을 Enabled State 객체로 설정
private AutoCommitBehavior autoCommitState = enabled;
public Statement createStatement() throws SQLException
{
return new JDBCStatement(database);
}
public void close() throws SQLException
{
try
{
autoCommitState.close();
database.dump();
database=null; // make the memory reclaimable and
}
catch(IOException e)
{
throw new SQLException( e.getMessage() );
}
}
public void commit() throws SQLException
{
autoCommitState.commit();
}
public void rollback() throws SQLException
{
autoCommitState.rollback();
}
public void setAutoCommit( boolean enable ) throws SQLException
{
autoCommitState.setAutoCommit(enable);
}
- JDBCConnection 클래스의 네 개 메소드 (Close, commit, rollback, setAutoCommit)의 행위가
커넥션이 오토 커밋 되어 있는지(AutoCommitBehavior enabled) 아닌지(AutoCommitBehavior disabled)에 따라 바뀌게 된다.
=> 새로운 State가 추가되거나 State의 code가 변경되더라고 JDBCconnection Class는 수정이 필요 없어 진다.
/** Return true if auto-commit mode is enabled */
public boolean getAutoCommit() throws SQLException
{
return autoCommitState == enabled;
}
}
기본 아이디어
어떤 객체(Real Object)를 Real Subject와 동일한 인터페이스를 구현한 다른 중개 객체(Proxy)를 통해 접근하는 것이 땔는 편리할 수 있다는 것이다. 중개 객체는 게으를 초기화(값비싼 필드 등을 필요시 로딩)와 같은 일을 하게 된다.
/** A Map proxy that handles lazy instantiation of tables from the disk.**/
private final class TableMap implements Map
{
private final Map realMap;
public TableMap( Map realMap )
{
this.realMap = realMap;
}
}
TableMap이 자신이 포함하고 있는 realMap 객체의 프록시가 된다.
- 리모트 프록시(Remote proxy)
: 네트워크 연결의 한 종단에 존재하며 네트워크의 다른 쪽에 있는 객체와 동일한 인터페이스를 구현한다. 사용자는 프록시에 메시지를 보내며, 프록시가 이 메시지를 네트워크 너머에 있는 실제 객체로 전달하게 된다. 이때 사용자는 프록시를 네트워크 너머에 있는 리포트 객체라 생각한다. 프록시를 통해 일어나는 모든 통신은 투명하게 리포트 객체에 전달되기 때문이다. 자바 RME, COBRA 그리고 XML/SOAP 모두 리포트 프록시 아키택처를 사용하고 있다.
- 가상 프록시(virtual proxy)
: Java.awt.Image 클래스, 이는 로딩될 실제 이미지에 대한 프록시 기능한다. Runtime.getImage()는 백그라운드 스레드가 네트워크를 통해 실제 이미지를 다운로드 하고 있는 동안, 실제 이미지에 대한 프록시를 반환한다. 이미지가 전부 다운로드 되기 전에도 프록시(getImage()가 반환한 이미지)를 사용할 수 있다. 이미지 다운로드가 끝나년 getImage() 메소드는 프록시가 아닌 실제 이미지를 반환한다.
CF> Decorator 패턴, Proxy 패턴, Chain of Responsibility 패턴 : 정정구조 대신 의도를 보자
기본 정의
일련의 객체 집합이 잘 정의된 통로(Chain)를 통해 메시지를 전달함으로써 하나 이상의 객체에 메시지를 처리할 수 있는 기회를 준다. 주어진 메시지에 가장 적합한 객체가 메시지를 처리한다. 또한 하나 이상의 객체가 메시지를 처리하는 것도 가능하다.
기본 아이디어
Decorator 패턴은 '구현 상속(Extends)' 대신 '인터페이스 상속(Implements)/위임' 전략을 사용하여
깨지기 쉬운 기반 클래스 문제를 해결한다.
public Table select( Selector where )
{
Table resultTable = new ConcreteTable( null,
(String[]) columnNames.clone() );
//result table을 Selector의 전략에 맞게 꺼내오는 부분 생략
return new UnmodifiableTable(resultTable);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public Table select(Selector where, String[] requestedColumns )
{
Table resultTable = new ConcreteTable( null,
(String[]) requestedColumns.clone() );
//result table을 Selector의 전략에 맞게 꺼내오는 부분 생략
return new UnmodifiableTable(resultTable);
}
- ConcreteTable을 생성하고, 조회한 후에 UnmodifiableTalbe을 사용하여 실제 결과 테이블을 래핑하여 반환
public class UnmodifiableTable implements Table
{
//UnmodifialbeTalbe의 Wrapped 에 Assign 한다.
private Table wrapped;
public UnmodifiableTable( Table wrapped )
{
this.wrapped = wrapped;
}
/** Return an UnmodifieableTable that wraps a clone of the
* currently wrapped table. (A deep copy is used.)
*/
public Object clone() throws CloneNotSupportedException
{ UnmodifiableTable copy = (UnmodifiableTable) super.clone();
copy.wrapped = (Table)( wrapped.clone() );
return copy;
}
public int insert(String[] c, Object[] v ){ illegal(); return 0;}
public int insert(Object[] v ){ illegal(); return 0;}
public int insert(Collection c,Collection v ){ illegal(); return 0;}
public int insert(Collection v ){ illegal(); return 0;}
public int update(Selector w ){ illegal(); return 0;}
public int delete( Selector w ){ illegal(); return 0;}
public void begin ( ){ illegal(); }
public void commit (boolean all){ illegal(); }
public void rollback (boolean all){ illegal(); }
private final void illegal()
{
throw new UnsupportedOperationException();
}
public Table select(Selector w,String[] r,Table[] o)
{
return wrapped.select( w, r, o );
}
public Table select(Selector where, String[] requestedColumns)
{
return wrapped.select(where, requestedColumns );
}
public Table select(Selector where)
{
return wrapped.select(where);
}
public Table select(Selector w,Collection r,Collection o)
{
return wrapped.select( w, r, o );
}
public Table select(Selector w, Collection r)
{
return wrapped.select(w, r);
}
public Cursor rows()
{
return wrapped.rows();
}
public void export(Table.Exporter exporter) throws IOException
{
wrapped.export(exporter);
}
public String toString() { return wrapped.toString(); }
public String name() { return wrapped.name(); }
public void rename(String s){ wrapped.rename(s); }
public boolean isDirty() { return wrapped.isDirty(); }
public Table extract(){ return wrapped; }
}
- Decorater(UnmodifieableTable)은 Table을 수정하는 메소드(insert, update, delete)가 호출 될 때는 예외
- 나머지 메소드들은 래핑된 Decorater에 작업을 위임한다.
정리
UnmodifiableTable은 ConcreteTable에 있는 여러 메소드의 행위를 변화시켜 Table을 효과적으로 수정 불가능하게 만드는 것이다.
1. IsImmutable 플래그를 넣어 이 플래그를 검사하게 할 수도 있지만 이 해결방법은 너무 복잡하다.
2. 클래스를 상속하여 테이블을 수정할 수 있는 메소드들이 예외를 던지도록 오버라이딩할 수도 있었지만
이는 깨지기 쉬운 기반 클래스 문제를 야기할 수 있다.
a. 기반 클래스에 테이블을 수정할 수 있는 메소드를 추가했는데 , 파생 클래스에서 이 메소드를 오버라이딩하는 것을 깜빡?
b. 컴파일 타임 에러를 런타임 에러로 바꾸기 때문에 유지보수하기 어려운 코드를 만들 위험에 노출시키게 된다.
3. 구현 상속으로 발생할 수 있는 문제를 인터페이스 상속/위임 전략을 통해 해결
래핑된 객체가 구현하고 있는 인터페이스와 똑같은 인터페이스를 구현한 수에 필요한 경우 연산을 래핑되어 있는 객체에 위임
(래핑되는 객체와 인터페이스를 공유하지 못한 다면 해당 객체를 기반 클래스로 해야 할 것이다)
=> 이 방법은 Extends를 사용하지 않고 행위를 변경시킬 수 있도록 해주기 때문에 깨지기 쉬운 기반 클래스 문제를 해결해준다.
Java I/O 클래스 Decorator 패턴 - 압축되어 있는 바이트 스트림을 읽을 때 데코레이터 체인을 사용한 예
1. 바이트를 읽는다.
2. 버퍼링을 사용하여 읽기 작업을 보다 효율적으로 한다.
3. 바이트 스트림의 압축을 푼다.
자바는 위 문제를 세 개의 클래스를 이용하여 해결한다.
1. FileInputStream에서 시작하며, 다음과 같이 인스턴스화 한다.
try
{
InputStream in = new FileInputStream( "file.name" );
}
catch( IOException e )
{
System.err.println( "Couldn't open file.name" );
e.printStackTrace();
}
2. 버퍼링을 데코레이션(혹은 래핑) 전략을 사용하여 추가한다.
즉 InputStream 객체를 바이트를 버퍼링하는 또다른 InputStream 구현체를 사용하여 래핑한다.
래퍼에 바이트를 요청하면 래퍼는 다시 래핑되어 있는 스트림 객체에 여러 바이트를 요청한 후 이를 반환 한다.
try
{
InputStream in = new FileInputStream( "file.name" );
in = new BufferedInputStream( in );
}
catch( IOException e )
{
System.err.println( "Couldn't open file.name" );
e.printStackTrace();
}
3. 압축 해체는 또 다른 데코레이터를 사용하여 추가한다.
try
{
InputStream in = new FileInputStream( "file.name" );
in = new BufferedInputStream( in );
in = new GZipInputStream( in );
}
catch( IOException e )
{
System.err.println( "Couldn't open file.name" );
e.printStackTrace();
}
정리
- 이러한 해결 방법은 유연하다. 원하는 기능을 위해 여러 가지 데코레이터를 조합하여 사용하면 되는 것인다.
- 더 중요한 것은 각 데코레이터 자체는 단 하나의 문제를 해결하기 때문에 상대적으로 단순하다.
=> 결과적으로 데코레이터들은 작성 및 디버깅하기 쉬우며, 시스템에 다른 영향을 미치지 않으면서 수정이 용이
- 여러 데코레이터들을 조합하면 여러 다양한 기능을 사용할 수 있다. 데코레이터 자체는 작지만, 합쳐지면 강력하다.
- Decorator 패턴을 사용하면 향후 원하는 기능을 추가하기도 쉽다.
- 주요 목적은 구현 상속의 대안을 제공하는 것이다. 기반 클래스의 행위를 수정하거나 기능을 추가하려 한다면
Decorator 패턴의 도입을 신중히 검토해 보자
- Decorator들을 래핑하는 순서도 중요하다.
Java Collection 클래스 decorator 패턴 사용의 예
스레드 안정성을 추가하기 위해 핵심 컬렉션 클래스들을 어떻게 확장(Extends)했는지 보여준다.
결과적으로 구체 클래스들의 숫자가 2배로 늘어났다.
컬렉션에 불변 기능을 추가한다면 아마도 Lock() 메소드를 추가하여 컬렉션을 수정하는 메소드들이 예외를 던지게 할 수 있을 것이다. 하지만 구현 상속을 이용하여 이러한 수정을 했을 때 구체 클래스의 숫자는 다시 두 배가 된다.
Collection 프레임워크 디자이너는 이와 같은 문제를 Decorator 패턴을 사용하여 해결하였다.
Collection threadSafe =
Collections.synchronizedCollection( new LinkedList() );
The Collections class looks something like this:
public class Collections // utility
{
//...
public static Collection synchronizedCollection(final Collection wrapped)
{
return new SynchronizedCollection(wrapped);
}
private static class SynchronizedCollection implements Collection
{
private Collection unsafe;
public SynchronizedCollection( Collection unsafe )
{
this.unsafe = unsafe;
}
public synchronized boolean add( Object toAdd )
{
return unsafe.add( toAdd );
}
public synchronized boolean remove( Object toAdd )
{
return unsafe.remove( toAdd );
}
// Implement synchronized version of all other Collection
// methods here ...
}
}
다음과 같이 이중 래핑을 통해 동기화된 불편 컬렉션을 만들 수도 있다.
Collection myList = new LinkedList();
//...
myList = Collections.synchronizedCollection
(
Collections.unmodifiableCollection
(
myList
)
);
=> Decorator 패턴의 핵심은 총 2개의 클래스를 추가하는 것 만으로도 구현 상속 시 18개의 클래스를 필요로 했던 문제 해결
정리
- Decorator 패턴의 핵심은 객체에 새로운 능력을 추가하거나 객체의 행위를 변화시킬 수 있도록 해준다.
- Decorator 패턴에서는 상속대신 합성을 사용하여 클래스 계층 구조를 단순화시킨다.