none
有個方法有部分共同代碼,部分不同代碼,應該要怎麼以物件導向的方式去撰寫會比較好? RRS feed

  • 問題

  • 文長!
    這個案例主要是描述有個方法有部分共同代碼,部分不同代碼,應該要怎麼以物件導向的方式去撰寫會比較好?

    案例描述:
    我有一個訂單服務,其中有個方法會更新訂單明細的數量,
    更新數量這個動作,會額外幫忙檢查數量規則,還有紀錄LOG,

    		public class OrderService
    		{
    			public void UpdateDetail(OrderDetail detail, int quantity)
    			{
    				if (quantity < 0)
    					throw new ArgumentException($"{nameof(quantity)}不可小於0");
    
    				detail.Quantity = quantity;
    				UpdateQuantity(detail);
    
    				Log(detail);
    			}
    
    			private void UpdateQuantity(OrderDetail orderDetail)
    			{
    				// ... 假裝有更新數量至DB
    			}
    
    			private void Log(OrderDetail orderDetail)
    			{
    				// ... 假裝紀錄 LOG 至DB
    			}
    		}
    
    		public class OrderDetail
    		{
    			public int Quantity { get; set; }
    			public bool IsPriority { get; set; }
    		}

    在不同模組中,都會有更新訂單明細數量的功能,所以會想重複使用 OrderService 來更新數量,
    但不同模組在更新數量裡面有差異的是,一個可單純更新數量,一個除了更新數量,還要標記 IsPriority = true,
    正常想要做到這兩種不同功能,不經思考就會直接多加個參數,然後判斷要不要設定 IsPriority 並更新這欄位,

    多加了 isPriority 參數與判斷,跟 UpdateDetail 方法

    			public void UpdateDetail(OrderDetail detail, int quantity, bool? isPriority = null)
    			{
    				if (quantity < 0)
    					throw new ArgumentException($"{nameof(quantity)}不可小於0");
    
    				detail.Quantity = quantity;
    
    				// 這裡不是使用 Entity Framework,是直接下 SQL,為了只執行一次SQL,固這樣寫
    				if (isPriority.HasValue)
    				{
    					detail.IsPriority = isPriority.Value;
    					UpdateDetail(detail);
    				}
    				else
    				{
    					UpdateQuantity(detail);
    				}
    
    				Log(detail);
    			}
    
    			private void UpdateDetail(OrderDetail orderDetail)
    			{
    				// ... 假裝有更新數量與優先屬性至DB
    			}

    但這樣寫我覺得不是很好,之後要是有更多情況,可能還要更新其他欄位,參數可能就一直增長,
    可能就把要更新的參數就替換成類別,

    			public void UpdateDetail(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				if (param.Quantity < 0)
    					throw new ArgumentException($"{nameof(param.Quantity)}不可小於0");
    
    				detail.Quantity = param.Quantity;
    				
    				if (param.IsPriority.HasValue)
    				{
    					detail.IsPriority = param.IsPriority.Value;
    					UpdateDetail(detail);
    				}
    				else
    				{
    					UpdateQuantity(detail);
    				}
    
    				Log(detail);
    			}
    
    		public class OrderDetailUpdateContent
    		{
    			public int Quantity { get; set; }
    			public bool? IsPriority { get; set; }
    		}


    這樣方法參數感覺好多了,但方法內判斷 IsPriority 的部分還是感覺不好,
    我原本想說,那 UpdateDetail 方法乾脆改為抽象方法,但方法裡面的檢查與LOG是希望固定就有的動作,
    如果 UpdateDetail 改成抽象方法,這樣實作抽象方法的人,還得注意要加上檢查跟LOG,
    為了讓固定的檢查與LOG保留著,只改更新數量差異的部分,就改為抽象類別,由不同模組去實作差異的更新功能 UpdateQuantity 方法,

    		public class AOrderService : OrderService
    		{
    			protected override void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				detail.Quantity = param.Quantity;
    				Update(detail);
    			}
    
    			protected void Update(OrderDetail orderDetail)
    			{
    				// ... 更新內容
    			}
    		}
    
    		public class BOrderService : OrderService
    		{
    			protected override void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				detail.Quantity = param.Quantity;
    				detail.IsPriority = param.IsPriority;
    				Update(detail);
    			}
    
    			protected void Update(OrderDetail orderDetail)
    			{
    				// ... 更新內容
    			}
    		}
    
    		public abstract class OrderService
    		{
    			public void UpdateDetail(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				if (param.Quantity < 0)
    					throw new ArgumentException($"{nameof(param.Quantity)}不可小於0");
    
    				UpdateQuantity(detail, param);
    
    				Log(detail);
    			}
    
    			protected abstract void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param);
    			
    			private void Log(OrderDetail orderDetail)
    			{
    				// ... 假裝紀錄 LOG 至DB
    			}
    		}

    但我覺得只是更新數量的部分有差異,而且 OrderService 可能有其他功能,
    這樣要使用其他功能的時候,要怎麼挑選衍生的類別(AOrderService Or BOrderService)?
    另外 UpdateQuantity 變成抽象方法後,這樣外部也能直接呼叫這方法,會不會呼叫到這個UpdateQuantity方法,卻沒做到檢查與LOG?
    所以我將這種更新差異所實作的類別,改到 UpdateDetail 的參數,並新增 OrderDetailQuantityUpdateBase 抽象類別,
    原本的 OrderService 就不是抽象類別,讓其他不是更新數量的功能也能直接使用,

    		public abstract class OrderDetailQuantityUpdateBase
    		{
    			public abstract void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param);
    		}
    
    		public class AOrderDetailQuantityUpdate : OrderDetailQuantityUpdateBase
    		{
    			public override void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				detail.Quantity = param.Quantity;
    				Update(detail);
    			}
    
    			protected void Update(OrderDetail orderDetail)
    			{
    				// ... 更新內容
    			}
    		}
    
    		public class BOrderDetailQuantityUpdate : OrderDetailQuantityUpdateBase
    		{
    			public override void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				detail.Quantity = param.Quantity;
    				detail.IsPriority = param.IsPriority;
    				Update(detail);
    			}
    
    			protected void Update(OrderDetail orderDetail)
    			{
    				// ... 更新內容
    			}
    		}
    
    		public class OrderService
    		{
    			public void UpdateDetail(OrderDetailQuantityUpdateBase quantityUpdate, OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				if (param.Quantity < 0)
    					throw new ArgumentException($"{nameof(param.Quantity)}不可小於0");
    
    				quantityUpdate.UpdateQuantity(detail, param);
    
    				Log(detail);
    			}
    			
    			private void Log(OrderDetail orderDetail)
    			{
    				// ... 假裝紀錄 LOG 至DB
    			}
    		}

    接著我就想 OrderDetailQuantityUpdateBase 是抽象類別,但我沒有要繼承一些基底的方法,沒有什麼關聯性,我覺得這邊可能換成 Interface 會更彈性,

    		public interface IOrderDetailUpdate
    		{
    			void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param);
    		}
    
    		public class AOrderDetailQuantityUpdate : IOrderDetailUpdate
    		{
    			public void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				detail.Quantity = param.Quantity;
    				Update(detail);
    			}
    
    			protected void Update(OrderDetail orderDetail)
    			{
    				// ... 更新內容
    			}
    		}
    
    		public class BOrderDetailQuantityUpdate : IOrderDetailUpdate
    		{
    			public void UpdateQuantity(OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				detail.Quantity = param.Quantity;
    				detail.IsPriority = param.IsPriority;
    				Update(detail);
    			}
    
    			protected void Update(OrderDetail orderDetail)
    			{
    				// ... 更新內容
    			}
    		}
    
    		public class OrderService
    		{
    			public void UpdateDetail(IOrderDetailUpdate quantityUpdate, OrderDetail detail, OrderDetailUpdateContent param)
    			{
    				if (param.Quantity < 0)
    					throw new ArgumentException($"{nameof(param.Quantity)}不可小於0");
    
    				quantityUpdate.UpdateQuantity(detail, param);
    
    				Log(detail);
    			}
    			
    			private void Log(OrderDetail orderDetail)
    			{
    				// ... 假裝紀錄 LOG 至DB
    			}
    		}

    若大大們有建議或是指出思考上的盲點,請不吝嗇給我留言,對物件導向設計還是一知半解,設計出來的代碼可能很多地方不對,感謝!!!

    2016年9月13日 下午 05:27

解答

  • public abstract  class BaseClass
        {
    
            // 由衍生類別決定怎麼做
            protected abstract void DoSomething();
    
            protected  void SomethingMustDo()
            {
                Console.WriteLine("這一定會做");
            }
    
            public void RealDo()
            {
                DoSomething();
                SomethingMustDo();
            }
        }
    
    
        public class Class1 : BaseClass
        {
            protected override void DoSomething()
            {
                Console.WriteLine("我在 Class1 這麼做");
            }
        }
    
        public class Class2 : BaseClass
        {
            protected override void DoSomething()
            {
                Console.WriteLine("我在 Class2 這麼做");
            }
        }

    聽過 Template Method 嗎 ? 他就是做你想要做的事情. 上面的程式碼就在實作 Template Method

    對於 呼叫端來講, 他只知道 RealDo. 然後再 RealDo 裡做手腳.


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2016年9月14日 上午 11:40
    版主

所有回覆

  • 共同的功能可以製作成父類別的方法, 不同的功能也製作成父類別的方法, 但是加上visual或abstract, 允許衍生類別override
    2016年9月14日 上午 06:02
  • Hi tihs,

    感謝回覆,這是繼承類別後的 override 的範例,但我想說希望預設呼叫 Update 方法就應該要去執行 CommonMethod,主要是怕 override 的人忘記呼叫 CommonMethod,請問這樣應該要怎麼做會比較好?

    還是這樣希望固定底層就會幫忙呼叫 CommonMethod 的設計不好?

    		public class CarBase
    		{
    			public virtual void Update()
    			{
    				// Update 要做的事情
    				Console.WriteLine("已更新 Car Base");
    
    				// 額外固定要做的事情
    				CommonMethod();
    			}
    
    			protected void CommonMethod()
    			{
    
    			}
    		}
    
    
    		public class MyCarBase : CarBase
    		{
    			public override void Update()
    			{
    				// Update 要做的事情
    				Console.WriteLine("已更新 My Car");
    
    				// 額外固定要做的事情
    				CommonMethod();
    			}
    		}


    2016年9月14日 上午 08:47
  • public abstract  class BaseClass
        {
    
            // 由衍生類別決定怎麼做
            protected abstract void DoSomething();
    
            protected  void SomethingMustDo()
            {
                Console.WriteLine("這一定會做");
            }
    
            public void RealDo()
            {
                DoSomething();
                SomethingMustDo();
            }
        }
    
    
        public class Class1 : BaseClass
        {
            protected override void DoSomething()
            {
                Console.WriteLine("我在 Class1 這麼做");
            }
        }
    
        public class Class2 : BaseClass
        {
            protected override void DoSomething()
            {
                Console.WriteLine("我在 Class2 這麼做");
            }
        }

    聽過 Template Method 嗎 ? 他就是做你想要做的事情. 上面的程式碼就在實作 Template Method

    對於 呼叫端來講, 他只知道 RealDo. 然後再 RealDo 裡做手腳.


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2016年9月14日 上午 11:40
    版主
  • 現代物件導向的主要精神是 "抽象", 所以不要被"狹隘的共用觀念"影響.

    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2016年9月14日 上午 11:48
    版主
  • public abstract  class BaseClass
        {
    
            // 由衍生類別決定怎麼做
            protected abstract void DoSomething();
    
            protected  void SomethingMustDo()
            {
                Console.WriteLine("這一定會做");
            }
    
            public void RealDo()
            {
                DoSomething();
                SomethingMustDo();
            }
        }
    
    
        public class Class1 : BaseClass
        {
            protected override void DoSomething()
            {
                Console.WriteLine("我在 Class1 這麼做");
            }
        }
    
        public class Class2 : BaseClass
        {
            protected override void DoSomething()
            {
                Console.WriteLine("我在 Class2 這麼做");
            }
        }

    聽過 Template Method 嗎 ? 他就是做你想要做的事情. 上面的程式碼就在實作 Template Method

    對於 呼叫端來講, 他只知道 RealDo. 然後再 RealDo 裡做手腳.


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    原來這叫做 Template Method,案例描述的第四段 Code 我有用這種方法嘗試過。

    假設如果這 DoSomething 方法只會用在 RealDo 方法上,

    衍生類別的其他方法就會看到 DoSomething 這方法,

    覺得 DoSomething  可能不適用於衍生類別的其他方法中,其他方法只希望看到 RealDo,

    所以我後面案例描述的代碼也嘗試用方法參數的抽象類別或介面方式來傳遞差異功能,

    主要是希望 RealDo 能呼叫到不同實作的 DoSomething就好,

    另外考慮到如果是這種案例還有 RealDo2、RealDo3,衍生類別要實作的方法就多了,在想會不會更複雜,單純實作某個差異在透過參數傳遞近來會比較好嗎?

    物件導向真的好難,可能案例描述的跟物件導向概念差很多,只是拿了物件導向的某些特性,去拼湊出我想像的 Template Method 功能,這我會試用在實際部分功能裡面,觀察差異,感謝回覆!


    2016年9月14日 下午 02:01
  • (1)  一路往下衍生類別並不是件好事.

    (2) 假設有個類別 Class3 是繼承 Class1, 但卻又不想看到 Class1 已經實作的 DoSomething Method, 那這個繼承就是錯的.

    (3) 招式是死的, 發招的人是活的,  要怎麼設計, 要看全貌, 也就是看要解決的本質是甚麼.

    (4) 另外一種做法就是 "傳遞委派給 RealDo", 也就是實際的方法在外面實作, 只是傳進來


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2016年9月14日 下午 02:14
    版主
  • (1)  一路往下衍生類別並不是件好事.

    (2) 假設有個類別 Class3 是繼承 Class1, 但卻又不想看到 Class1 已經實作的 DoSomething Method, 那這個繼承就是錯的.

    (3) 招式是死的, 發招的人是活的,  要怎麼設計, 要看全貌, 也就是看要解決的本質是甚麼.

    (4) 另外一種做法就是 "傳遞委派給 RealDo", 也就是實際的方法在外面實作, 只是傳進來


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    嗯嗯!! 

    第4點我一開始想說用委派的話,不管是匿名方法委派還是具名方法委派,如果這個實作方法也會重複使用的話,覺得直接傳遞實作的類別也不錯!

    2016年9月14日 下午 02:28