[Architecture] : Entity Expansion
動機 :
一個軟體系統的生命週期,必然面臨到系統改版的問題。而在系統改版的時候,最常遇到的問題之一是,使用者希望增加系統物件的資料欄位(例如 : 使用者資料增加相片)。常見的做法是把相關的功能,從把整個系統從UI到DB重整(重寫?)一遍,讓使用者希望增加至系統的欄位,在系統裡實現。
這樣的做法,筆者把它稱作『修改式程式碼累積』。所謂的修改式程式碼累積是說,藉由修改經過驗證、並且正常運作的程式碼與介面來擴充系統。理論上,程式碼經過修改之後,必須重新執行完整的測試。而介面經過修改,使用手冊、教育訓練等等,常常也必需做同步的更新。可以說是牽一髮動全身。
筆者比較喜愛『增加式程式碼累積』。所謂的增加式程式碼累積是說,原有的程式碼、包含介面不做更動,而是增加額外的程式碼附加到系統內來擴充系統。 因為程式碼與介面都沒有做過修改,就可以避免修改程式碼所產生的額外工作。但不可避免的,這樣的系統在開發初期要花比較大的心力做設計。
本文介紹一個Entity Expansion模式。Entity Expansion模式主要是定義一組,資料物件(Entity)以及邊界物件(Repository)的生成、結構、行為模式,用來擴展物件的屬性資料。實作這個模式,可以為系統加入增加式程式碼累積的能力。
* 下列文章分別標註段落為01、02只是不想全部寫成一個超長段落,不是示意專案必須拆解成兩個。
基礎平台 :
結構
參與者
Page Shell
-頁面的殼層,可以透過設定資料,動態掛載系統頁面的系統。
範例程式
Page Shell依照開發平台的不同,會有不同的選擇。例如以ASP.NET的平台來說,可以直接套用ASP.NET的頁面機制,然後再另外建立一個索引頁面,用修改索引頁面的方式來達成動態掛載系統頁面的需求。
基礎專案01 :
結構
參與者
UserManagePage
-User管理頁面,提供新增、修改、刪除、查詢 User的功能。
-使用例如 Spring.Net、Provider Pattern來反射生成 IUserRepositoryFactory。
-使用生成的 IUserRepositoryFactory生成 IUserRepository。
-使用 IUserRepository新增、修改、刪除、查詢 User。
User
-系統運作使用的資料物件。
IUserRepositoryFactory
-生成 IUserRepository。
-依靠例如 Spring.Net、Provider Pattern來反射生成。
IUserRepository
-資料物件 User進出系統邊界的介面。
-提供新增、修改、刪除、查詢等功能。
範例程式
namespace CLK.EntityExpansion
{
public class UserManagePage
{
// Methods
public void ShowUser()
{
// GetData
IUserRepositoryFactory userRepositoryFactory = null; // 使用例如Spring.Net、Provider Pattern來反射生成。(Base專案生成 DefaultUserRepositoryFactory、Ex專案生成 ExpandedUserRepositoryFactory)
IUserRepository userRepository = userRepositoryFactory.CreateRepository();
IEnumerable<User> userCollection = userRepository.GetAll();
// Show
this.ShowUser(userCollection);
}
private void ShowUser(IEnumerable<User> userCollection)
{
//.....
}
}
public class User
{
// Constructor
public User()
{
this.UserID = Guid.Empty;
this.Name = string.Empty;
this.Description = string.Empty;
}
protected User(User item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
this.UserID = item.UserID;
this.Name = item.Name;
this.Description = item.Description;
}
// Properties
public Guid UserID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public interface IUserRepositoryFactory
{
// Methods
IUserRepository CreateRepository();
}
public interface IUserRepository
{
// Methods
void Add(User item);
void Modify(User item);
void Remove(Guid id);
User GetByID(Guid id);
IEnumerable<User> GetAll();
}
}
基礎專案02 :
結構
參與者
DefaultUserRepositoryFactory
-IUserRepositoryFactory的實作
-生成 IUserRepository - SqlUserRepository。
SqlUserRepository
-資料物件 User進出系統邊界的介面,IUserRepository的實作。
-依靠 IUserRepositoryFactory生成。
-負責將資料物件User進出SQL資料庫。
範例程式
namespace CLK.EntityExpansion
{
public class DefaultUserRepositoryFactory : IUserRepositoryFactory
{
// Methods
public IUserRepository CreateRepository()
{
// Create
IUserRepository userRepository = new SqlUserRepository();
// Return
return userRepository;
}
}
public class SqlUserRepository : IUserRepository
{
// Methods
public void Add(User item)
{
// Sql操作...
}
public void Modify(User item)
{
// Sql操作...
}
public void Remove(Guid id)
{
// Sql操作...
}
public User GetByID(Guid id)
{
// Sql操作...
return null;
}
public IEnumerable<User> GetAll()
{
// Sql操作...
return null;
}
}
}
擴展專案01 :
結構
參與者
ExUserManagePage
-ExUser管理頁面,提供新增、修改、刪除、查詢 ExUser的功能。
-使用例如 Spring.Net、Provider Pattern來反射生成 IExUserRepositoryFactory。
-使用生成的 IExUserRepositoryFactory生成 IExUserRepository。
-使用 IExUserRepository新增、修改、刪除、查詢 ExUser。
ExUser
-系統運作使用的資料物件。
-繼承User並且擴展自己的屬性資料。
IUserRepositoryFactory
-生成 IExUserRepository。
-依靠例如 Spring.Net、Provider Pattern來反射生成。
IExUserRepository
-資料物件 ExUser進出系統邊界的介面。
-提供新增、修改、刪除、查詢等功能。
範例程式
namespace CLK.EntityExpansion.Expanded
{
public class ExUserManagePage
{
// Methods
public void ShowExUser()
{
// GetData
IExUserRepositoryFactory userRepositoryFactory = null; // 使用例如Spring.Net、Provider Pattern來反射生成。(Base專案生成 DefaultExUserRepositoryFactory、Ex專案生成 ExpandedExUserRepositoryFactory)
IExUserRepository userRepository = userRepositoryFactory.CreateRepository();
IEnumerable<ExUser> userCollection = userRepository.GetAll();
// Show
this.ShowExUser(userCollection);
}
private void ShowExUser(IEnumerable<ExUser> exUserCollection)
{
//.....
}
}
public class ExUser : User
{
// Constructor
public ExUser()
: base()
{
this.Photo = null;
}
public ExUser(User item)
: base(item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
this.Photo = null;
}
// Properties
public byte[] Photo { get; set; }
}
public interface IExUserRepositoryFactory
{
// Methods
IExUserRepository CreateRepository();
}
public interface IExUserRepository
{
// Methods
void Add(ExUser item);
void Modify(ExUser item);
void Remove(Guid id);
ExUser GetByID(Guid id);
IEnumerable<ExUser> GetAll();
}
}
擴展專案02 :
結構
參與者
DefaultExUserRepositoryFactory
-IExUserRepositoryFactory的實作。
-生成 IUserRepository - SqlUserRepository。
-生成 IExUserSectionRepository - SqlExUserSectionRepository。
-生成 ExUserRepository,傳入生成的IUserRepository、IExUserSectionRepository。
ExUserRepository
-資料物件 ExUser進出系統邊界的介面,IExUserRepository的實作。
-依靠 DefaultExUserRepositoryFactory生成,接受傳入的IUserRepository、IExUserSectionRepository。
-負責資料物件ExUser拆解為 User及ExUserSection。並將使用者對其新增修改刪除動作拆解為,User物件進出IUserRepository、ExUserSection物件進出IExUserSectionRepository。
-負責資料物件User及ExUserSection組合為ExUser。並將使用者對其查詢動作拆解為,User物件進出IUserRepository、ExUserSection物件進出IExUserSectionRepository。
ExUserSection
-系統運作使用的資料物件,存放ExUser擴充增加的屬性資料。
IExUserSectionRepository
-資料物件 ExUserSection進出系統邊界的介面。
-提供新增、修改、刪除、查詢等功能。
SqlExUserSectionRepository
-資料物件ExUserSection 進出系統邊界的介面,IUserRepository的實作。
-負責將資料物件 ExUserSection進出SQL資料庫。
ExpandedUserRepositoryFactory
-IUserRepositoryFactory的實作。
-生成 IUserRepository - SqlUserRepository。
-生成 IExUserSectionRepository - SqlExUserSectionRepository。
-生成 UserRepositoryDecorator傳入生成的IUserRepository、IExUserSectionRepository。
UserRepositoryDecorator
-資料物件 User進出系統邊界的介面,IUserRepository的實作。
-依靠 IUserRepositoryFactory生成。
-依靠 ExpandedUserRepositoryFactory生成,接受傳入的IUserRepository、IExUserSectionRepository。
-負責將使用者對資料物件User新增修改刪除查詢動作拆解為進出IUserRepository。
-負責資料物件User拆解為 UserId。並將使用者對其刪除動作拆解為,ExUserSection物件自IExUserSectionRepository移除。
範例程式
namespace CLK.EntityExpansion.Expanded
{
public class ExUserSection
{
// Properties
public Guid UserID { get; set; }
public byte[] Photo { get; set; }
}
public interface IExUserSectionRepository
{
// Methods
void Add(ExUserSection item);
void Modify(ExUserSection item);
void Remove(Guid id);
ExUserSection GetByID(Guid id);
}
public class SqlExUserSectionRepository : IExUserSectionRepository
{
public void Add(ExUserSection item)
{
// Sql操作...
}
public void Modify(ExUserSection item)
{
// Sql操作...
}
public void Remove(Guid id)
{
// Sql操作...
}
public ExUserSection GetByID(Guid id)
{
// Sql操作...
return null;
}
}
}
namespace CLK.EntityExpansion.Expanded
{
public class DefaultExUserRepositoryFactory : IExUserRepositoryFactory
{
// Methods
public IExUserRepository CreateRepository()
{
// Create
IUserRepository userRepository = new SqlUserRepository();
IExUserSectionRepository exUserSectionRepository = new SqlExUserSectionRepository();
// Return
return new ExUserRepository(userRepository, exUserSectionRepository);
}
}
public class ExUserRepository : IExUserRepository
{
// Fields
private readonly IUserRepository _userRepository = null;
private readonly IExUserSectionRepository _exUserSectionRepository = null;
// Constructor
public ExUserRepository(IUserRepository userRepository, IExUserSectionRepository exUserSectionRepository)
{
#region Require
if (userRepository == null) throw new ArgumentNullException();
if (exUserSectionRepository == null) throw new ArgumentNullException();
#endregion
_userRepository = userRepository;
_exUserSectionRepository = exUserSectionRepository;
}
// Methods
public void Add(ExUser item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
// User
_userRepository.Add(item);
// ExUserSection
ExUserSection exUserSection = new ExUserSection();
exUserSection.UserID = item.UserID;
exUserSection.Photo = item.Photo;
_exUserSectionRepository.Add(exUserSection);
}
public void Modify(ExUser item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
// User
_userRepository.Modify(item);
// ExUserSection
ExUserSection exUserSection = new ExUserSection();
exUserSection.UserID = item.UserID;
exUserSection.Photo = item.Photo;
_exUserSectionRepository.Modify(exUserSection);
}
public void Remove(Guid id)
{
#region Require
if (id == Guid.Empty) throw new ArgumentNullException();
#endregion
// User
_userRepository.Remove(id);
// ExUserSection
_exUserSectionRepository.Remove(id);
}
public ExUser GetByID(Guid id)
{
#region Require
if (id == Guid.Empty) throw new ArgumentNullException();
#endregion
// User
User user = _userRepository.GetByID(id);
if (user == null) return null;
// ExUserSection
ExUserSection exUserSection = _exUserSectionRepository.GetByID(id);
if (exUserSection == null) return new ExUser(user);
// ExUser
ExUser exUser = new ExUser(user);
exUser.Photo = exUserSection.Photo;
return exUser;
}
public IEnumerable<ExUser> GetAll()
{
// Result
List<ExUser> exUserList = new List<ExUser>();
// User
foreach (User user in _userRepository.GetAll())
{
// ExUserSection
ExUserSection exUserSection = _exUserSectionRepository.GetByID(user.UserID);
if (exUserSection == null)
{
exUserList.Add(new ExUser(user));
continue;
}
// ExUser
ExUser exUser = new ExUser(user);
exUser.Photo = exUserSection.Photo;
exUserList.Add(exUser);
}
// Return
return exUserList;
}
}
}
namespace CLK.EntityExpansion.Expanded
{
public class ExpandedUserRepositoryFactory : IUserRepositoryFactory
{
// Methods
public IUserRepository CreateRepository()
{
// Create
IUserRepository userRepository = new SqlUserRepository();
IExUserSectionRepository exUserSectionRepository = new SqlExUserSectionRepository();
// Return
return new UserRepositoryDecorator(userRepository, exUserSectionRepository);
}
}
public class UserRepositoryDecorator : IUserRepository
{
// Fields
private readonly IUserRepository _userRepository = null;
private readonly IExUserSectionRepository _exUserSectionRepository = null;
// Constructor
public UserRepositoryDecorator(IUserRepository userRepository, IExUserSectionRepository exUserSectionRepository)
{
#region Require
if (userRepository == null) throw new ArgumentNullException();
if (exUserSectionRepository == null) throw new ArgumentNullException();
#endregion
_userRepository = userRepository;
_exUserSectionRepository = exUserSectionRepository;
}
// Methods
public void Add(User item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
// User
_userRepository.Add(item);
}
public void Modify(User item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
// User
_userRepository.Modify(item);
}
public void Remove(Guid id)
{
#region Require
if (id == Guid.Empty) throw new ArgumentNullException();
#endregion
// User
_userRepository.Remove(id);
// ExUserSection
_exUserSectionRepository.Remove(id);
}
public User GetByID(Guid id)
{
#region Require
if (id == Guid.Empty) throw new ArgumentNullException();
#endregion
// User
User user = _userRepository.GetByID(id);
// Return
return user;
}
public IEnumerable<User> GetAll()
{
// Result
IEnumerable<User> userList = null;
// User
userList = _userRepository.GetAll();
// Return
return userList;
}
}
}
結語 :
本文範例簡單的說就是,當基礎專案與擴展專案都建立完畢。在原本基礎專案的管理頁面建立User物件,在擴展專案的ExUser管理頁面也可以查詢到新增的ExUser(內容為User資料,ExUser擴展的屬性資料為預設值)。當在擴展專案刪除ExUser時,基礎專案內的User也會同時被刪除。
本文介紹的Entity Expansion模式,看起來只是擴展了物件的屬性資料,但其實可以看成,處理了強型別擴展物件進出系統邊界的責任。以此模式為基礎發展,理論上可以設計出能無限延伸的應用系統架構。
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。