요즘 대부분의 프로그램에서 Undo, Redo 기능을 지원하고 있습니다.
고객사에서 Undo 기능이 제대로 동작하지 않는다고 오류 접수를 해서 코드를 확인해 보니 아래와 같이 되어 있었습니다.
Item 속성 밑에 있는 것들은 모두 CommentUserInfo 클래스의 속성들 중 일부였습니다.
아마도 이 코드를 작성한 개발자는 Undo, Redo 기능을 개발하면서 필요한 속성들을 그때 그때 추가한 것 같았습니다.
'LineSize는 적용되는데 DashSize는 적용 안되네'하고 DashSize를 추가하고 마찬가지로 Opacity, Angle도 그렇게 추가한 것 같았습니다.
나중에 추가 기능이 필요해서 CommentUserInfo에 속성을 추가하면 UndoRedoData에도 속성을 추가해야 합니다.
관리해야 할 지점이 2군데가 되고 그만큼 버그가 발생할 확률도 높아집니다.
이런 식의 작업은 계속 속성들을 추가할 것이고 결국에는 CommentUserInfo와 같아질 것입니다.
따라서 UndoRedoData에 CommentUserInfo의 속성을 추가하는 것 보다 CommentUserInfo의 인스턴스를 추가하는 것이 좋습니다.
Undo, Redo이 개념은 쉽습니다.
어떤 행위를 하기 전의 상태를 저장해 두었다가 Undo를 하면 이전 상태로 되돌리고, Redo를 하면 행위가 일어난 후의 상태로 되돌리면 됩니다.
State에 행위가 일어나기 전 혹은 후의 상태를 저장해야 하기 때문에 CommentUserInfo 클래스에 Clone() 함수를 추가합니다.
UndoRedoManager는 Singleton 패턴을 사용하여 하나의 인스턴스만 생성하도록 하였습니다.
고객사에서 Undo 기능이 제대로 동작하지 않는다고 오류 접수를 해서 코드를 확인해 보니 아래와 같이 되어 있었습니다.
public class UndoRedoData
{
/// 현재 화면에 보이는 아이템의 레퍼런스
public CommentUserInfo Item { get; set; }
/// 아래는 Undo, Redo를 하기 위한 속성들
public List<Point> PointSet { get; set; }
public double LineSize { get; set; }
public DoubleCollection DashSize { get; set; }
public Double Opacity { get; set; }
public Controls.Common.PaintSet paint { get; set; }
public double Angle { get; set; }
}
이 코드를 보는 순간 '잘못 되었구나'라는 생각이 들었습니다.Item 속성 밑에 있는 것들은 모두 CommentUserInfo 클래스의 속성들 중 일부였습니다.
아마도 이 코드를 작성한 개발자는 Undo, Redo 기능을 개발하면서 필요한 속성들을 그때 그때 추가한 것 같았습니다.
'LineSize는 적용되는데 DashSize는 적용 안되네'하고 DashSize를 추가하고 마찬가지로 Opacity, Angle도 그렇게 추가한 것 같았습니다.
나중에 추가 기능이 필요해서 CommentUserInfo에 속성을 추가하면 UndoRedoData에도 속성을 추가해야 합니다.
관리해야 할 지점이 2군데가 되고 그만큼 버그가 발생할 확률도 높아집니다.
이런 식의 작업은 계속 속성들을 추가할 것이고 결국에는 CommentUserInfo와 같아질 것입니다.
따라서 UndoRedoData에 CommentUserInfo의 속성을 추가하는 것 보다 CommentUserInfo의 인스턴스를 추가하는 것이 좋습니다.
public class UndoRedoData
{
public UndoRedoData(CommentUserInfo lhs)
{
Item = lhs;
}
public CommentUserInfo Item { get; set; }
public CommentUserInfo State { get; set; }
}
이렇게 Undo, Redo를 위한 자료 구조를 정의하였습니다.Undo, Redo이 개념은 쉽습니다.
어떤 행위를 하기 전의 상태를 저장해 두었다가 Undo를 하면 이전 상태로 되돌리고, Redo를 하면 행위가 일어난 후의 상태로 되돌리면 됩니다.
State에 행위가 일어나기 전 혹은 후의 상태를 저장해야 하기 때문에 CommentUserInfo 클래스에 Clone() 함수를 추가합니다.
public class CommentUserInfo
{
/*
클래스를 복제하여 리턴한다.
*/
public CommentUserInfo Clone()
{
var clone = new CommentUserInfo();
clone.Copy(this);
return clone;
}
/*
주어진 lhs를 복사한다.
*/
public void Copy(CommentUserInfo lhs)
{
...
}
}
마지막으로 Undo, Redo 스택을 관리하기 위한 Manager 클래스를 만듭니다.UndoRedoManager는 Singleton 패턴을 사용하여 하나의 인스턴스만 생성하도록 하였습니다.
public class UndoRedoManager
{
private Stack<UndoRedoData> _UndoStack {get;} = new Stack<UndoRedoData>();
private Stack<UndoRedoData> _RedoStack {get;} = new Stack<UndoRedoData>();
private static readonly Lazy<UndoRedoManager> lazy = new Lazy<UndoRedoManager>(() => new UndoRedoManager());
public static UndoRedoManager Instance
{
get { return lazy.Value; }
}
/*
클래스 외부에서 클래스를 생성하지 못하도록 private으로 선언
*/
private UndoRedoManager(){}
/*
행위가 일어나기 전의 상태를 저장한다.
*/
public void Push(UndoReoData lhs)
{
lhs.State = lhs.Item.Clone();
_UndoStack.Push(lhs);
}
/*
Undo를 실행한다.
*/
public void Undo()
{
if(_UndoStack.Any())
{
var data = _UndoStack.Pop();
#region swap Item with State
var tmp = data.Item.Clone();
data.Item.Copy(data.State);
data.State.Copy(tmp);
#endregion
/// RedoStack에 저장한다.
_RedoStack.Push(data);
}
}
/*
Redo를 실행한다.
*/
public void Redo()
{
if(_RedoStack.Any())
{
var data = _RedoStack.Pop();
#region swap Item with State
var tmp = data.Item.Clone();
data.Item.Copy(data.State);
data.State.Copy(tmp);
#endregion
/// UndoStack에 저장한다.
_UndoStack.Push(data);
}
}
}
Item의 상태가 바뀔 때 예를 들어 도형의 크기가 변경되거나 회전할 때 그 상태를 UndoRedoManager의 Push 함수로 저장합니다.var manager = UndoRedoManager::Instance;
manager.Push(new UndoRedoData(item));
Undo, Redo를 할 때는 UndoRedoManager의 Undo, Redo 함수를 호출하면 됩니다.
댓글
댓글 쓰기