[Architecture] Entity Expansion

[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模式,看起來只是擴展了物件的屬性資料,但其實可以看成,處理了強型別擴展物件進出系統邊界的責任。以此模式為基礎發展,理論上可以設計出能無限延伸的應用系統架構。

 

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。