顯示具有 軟體架構 標籤的文章。 顯示所有文章
顯示具有 軟體架構 標籤的文章。 顯示所有文章

2015年7月30日 星期四

軟體架構之觀察者模式


    使用者新增bug回報後或需求單,如果不主動登入網頁時就永遠無法知道誰回報了BUG,無法有主動通知的概念,想要有一個有主動通知的提醒功能,於是想到了觀察者的模式,以後不論有新的系統需要主動偵測時,在新增物件後,自動有擴充效果。 


情境1:
    當接收的人員已經按下確定後,警告視窗不需提醒,如果A人員已經看過並按下確定後,無法再彈出,但B人員尚未看過,仍不斷跳出提醒,確定每位人員都能看到bug回報。  
















情境3: 為了有擴充的功能,採用觀察者的樣式, 觀察者 (Observer) 與被觀察者 (Observable) 之間採用鬆偶合 (loose-coupling/鬆綁) 的方式結合。因為雙方有實踐特定介面,因此不用知道彼此的細節。 


本案例中testBugObserverreleaseBugObserver有各自邏輯檢查是否有該登入者尚未讀取,所以在observer的父類別,訂出抽象類別的方法findNoRead ,由繼承的子類別覆寫findNoRead的邏輯,父類別SupperSubjectattach負責把加入繼承supperObserver的物件,再由父類別的SupperSubjectlistAllNoRead將所有繼承supperObserver的物件findNoRead去搜尋未讀的資料。

程式範例 觀察者模式又稱(發布/訂閱)模式
父類別:訂閱者的角色
    public abstract class supperObserver
    {     
        public abstract IList<viewData> findNoRead(string userID);
    }
子類別:繼承父類別訂閱者
public class testBugObserver : supperObserver
    {     
        public override IList<viewData> findNoRead(string userID)
        {
            entityNotifyData entityModel = new entityNotifyData();
            IList<viewData> liData = entityModel.liBugNoReadTestData(userID); //讀取資料
            return liData.ToList();
        }
    }
子類別:繼承父類別訂閱者 
public class releaseBugObserver : supperObserver
    {
        public override IList<viewData> findNoRead(string userID)
        {
            entityNotifyData entityModel = new entityNotifyData();
            IList<viewData> liData = entityModel.liBugNoReadReleaseData(userID); //讀取資料
            return liData.ToList();
        }
    }

  
    子類別繼承父類別supperObserver覆寫父類別的findNoRead(string userID)讓物件有各自不同的邏輯,後續也容易可以擴充新的物件。

  public abstract class supperSubject
    {   
        /// 登入者的帳號
        public string userID { get;    set;  }
        /// 訂閱者們
        private IList<supperObserver> liObservers = new List<supperObserver>();
        public void attach(supperObserver Observer)
        {
            liObservers.Add(Observer);
        }
         /// 取回所有未讀資料
        public List<viewData> listAllNoRead()
        {
            List<viewData> liViewData = new List<viewData>();
            foreach (supperObserver observer in liObservers)
            {
// 物件本身的findNoRead 有各自的邏輯,以達到擴充的效果
                liViewData.AddRange(observer.findNoRead(userID));
            }
            return liViewData;
        }
    }


public class subject : supperSubject
    {

    }
   
    觀察者和被觀察者的父類別先定義好彼此的關係,父類別的supperSubject 有兩個方法attach listAllNoRead
l   attach 由繼承supperSubject 的子類別用attach的方式加入有繼承supperObserver 的物件到父類別IList<supperObserver> liObservers陣列集合

l   listAllNoRead : 由繼承supperSubject 的子類別用listAllNoRead 逐一把陣列集合中的物件,呼叫物件覆寫父類別supperObserverfindNoRead

private void timer1_Tick(object sender, EventArgs e)
        {
            _liAllNoRead = null//未讀取的資料集合         
            //出版者 (觀察者)
            subject subjectObj = new subject();
            subjectObj.userID = "0137";
            //訂閱者(被觀察者)
            testBugObserver bugObserverObj = new testBugObserver();
            releaseBugObserver applyObj = new releaseBugObserver();
            subjectObj.attach(bugObserverObj);
            subjectObj.attach(applyObj);          
            _liAllNoRead = subjectObj.listAllNoRead();  //取回被觀察者們傳回的未讀取的資料                      
            new Thread(() =>
            {
                foreach (viewData noRead in _liAllNoRead)
                {
                    notifyIcon1.BalloonTipIcon = ToolTipIcon.Info;
                    notifyIcon1.BalloonTipText = noRead.type;
                    notifyIcon1.BalloonTipTitle = noRead.title;
                    notifyIcon1.ShowBalloonTip(5000);
                    Thread.Sleep(6000);
                }
            }).Start();  
        }


















上圖的Windows Gridview資料分別由各自的被觀察者releaseBugObservertestBugObserver 自已的findNoRead 找出來的資料

在正式版的公告-->新增bug --> 右下角跳出提醒視窗



在測試版的公告-->新增bug --> 右下角跳出提醒視窗











    這樣以後不論有多少網站,需要有提醒功能,只要加上新的observer的物件,就不用再修改任何程式,一樣就有提醒功能,如果觀念誤導大家時,請各位前輩多多指導,學習的道路上本來就是永無止盡的,期望大家也能更上一層樓。

2015年2月10日 星期二

MVC進階體驗-範例流程表單

    因為公司內部的需求,需要有表單跑流程,作者以Asp.net MVC+Angular.js 快速開發有流程需求單,軟體架構是分層方式(控制層、實體層),將控制邏輯的程式碼放在控制層,把與資料的相關函式(儲存、修改、查詢資料)列在實體層,如此單一職責,更方便測試每一個函式邏輯。

一、    系統需求


流程圖


流程說明
1.    申請人: 所有人員需填寫申請單, 並發出MAIL通知
2.    申請人的主管: 負責核可/退回案件申請
3.    RD窗口: 選擇處理人員,再MAIL 通知處理人員
4.    處理人員:
l   同意à填寫完工日
l   不同意à填寫不同意的原因à發出Mail通知主管、申請人
5.    確認完工: 發出MAIL通知主管、申請人
6.    流程表單
l   流程1: 申請人à主管à RD窗口à負責處理人à完工確認
l   流程2: 申請人(主管)à RD窗口à負責處理人à完工確認
類別圖

    根據分層架構邏輯,作者分成控制層(controller)、實體實(entity),職責分離,再由asp.net MVC Controller 呼叫控制層類別


一、    實體層:單一職責

l   listRequests(): 列出所有表單的語法

    public IEnumerable<entityRequest> listRequests(string applyUserID, DateTime? startDate, DateTime? endDate)
        {
                var query = from p1 in tcsDB.applyRequests
                  join p2 in tcsDB.alluser on p1.applyUser equals p2.uid into userName
                  join p3 in tcsDB.alluser on p1.processUser equals p3.uid into flowUser
                  join p4 in tcsDB.applyRequestsAuthorization on p1.applyUser equals p4.userID into authorizeUser
                            from apply in userName.DefaultIfEmpty()
                            from process in flowUser.DefaultIfEmpty()
                            from authorize in authorizeUser.DefaultIfEmpty()
                            where (p1.applyUser == applyUserID || authorize.bossID == applyUserID)
                            orderby p1.requestDate descending
                             select new entityRequest
                            {
                                ikey = p1.ikey,
                                applyUserName = apply.name,
                                applyType = p1.applyType,
                                requestDate = p1.requestDate,
                                applyReason = p1.applyReason,
                                processName = p1.processName,
                                processFlow = p1.processFlow,
                                processUser = process.name,
                                accepTaskUser = p1.accepTaskUser,
                                processAction = p1.processAction
                            };
////設定起迄區間,可以後續增加where 條件
                if (startDate != null && endDate != null)
                {
                    query = query.Where(m => m.requestDate >= startDate && m.requestDate <= endDate);
                }
                var results = query.Select(n => new entityRequest
                {
                    ikey = n.ikey,
                    applyUserName = n.applyUserName,
                    applyType = n.applyType,
                    requestDate = n.requestDate,
                    applyReason = n.applyReason,
                    processName = n.processName,
                    processFlow = n.processFlow,
                    processUser = n.processUser,
                    accepTaskUser = n.accepTaskUser,
                    processAction = n.processAction,
                }).AsEnumerable();
                return results.AsEnumerable<entityRequest>();
            }
          }

l   doNextFlow (): 跳下一個流程
  public void doNextFlow(string strProcessType, int intApplyRequestID, DateTime? dTimePredictCompleteDate)
        {          
                var applyRequestObj = tcsDB.applyRequests.Find(intApplyRequestID);
                FlowItem nextFlowItem = entityProcessFlowObj.getFlowNext(strProcessType, applyRequestObj.processFlow); //找出下一個流程
                if (nextFlowItem == null)
                {
                    return;
                }
                applyRequestObj.processFlow = nextFlowItem.flowItemID; //下一個流程ID
                applyRequestObj.processName = nextFlowItem.flowItemName.Trim();//下一個流程名稱
                applyRequestObj.processAction = nextFlowItem.flowRoleFunc.Trim();//下一個流程動作
                //儲存預期日期
                if (dTimePredictCompleteDate != null)
                {
                    applyRequestObj.predictCompleteDate = dTimePredictCompleteDate;
                }
                entityFlowRole entityFlowRoleUser = new entityFlowRole();
                flowRole flowRoleUser = entityFlowRoleUser.getUserRole(applyRequestObj.ikey, applyRequestObj.applyUser, nextFlowItem.flowRoleFunc); //找出下一流程的處理者
                applyRequestObj.processUser = flowRoleUser.strRoleUserID.Trim();               
                tcsDB.SaveChanges();
            }
          }

l   相關動作處理行為
//同意
public  void  agree(string strProcessType, int intApplyRequestID)
        {
            doNextFlow(strProcessType, intApplyRequestID,null);//跑下一流程
        }
//不准
public  void  reject(string strProcessType, int intApplyRequestID, string strRejectReason)
        {          
                applyRequests applyRequestObj = tcsDB.applyRequests.Where(m => m.ikey == intApplyRequestID).FirstOrDefault();
                applyRequestObj.processAction = "reject";
                applyRequestObj.rejectReason = strRejectReason; //退回的理由
                applyRequestObj.completeDate = DateTime.Now;
                tcsDB.SaveChanges();
            }
//RD窗口
public void rdDispatch(string strProcessType, int intApplyRequestID,  string strRdAcceptTaskUserID)
        {          
                applyRequests applyRequestObj = tcsDB.applyRequests.Where(m => m.ikey == intApplyRequestID).FirstOrDefault();
                applyRequestObj.accepTaskUser = strRdAcceptTaskUserID;
                applyRequestObj.processAction = "rdDispatch";
                tcsDB.SaveChanges();
                doNextFlow(strProcessType, intApplyRequestID,null);         
        }
//完工
  public void complete(string strProcessType, int intApplyRequestID)
        {           
                applyRequests applyRequestObj = tcsDB.applyRequests.Where(m => m.ikey == intApplyRequestID).FirstOrDefault();
                applyRequestObj.processAction = "end";
                applyRequestObj.processName = "結束";
                applyRequestObj.completeDate = DateTime.Now;
                tcsDB.SaveChanges();
            }
        }


l   角色設定: 依據角色定義,傳回該角色的相關屬性
     public flowRole getUserRole(int intRequestID,string strApplyUserID, string strRoleName)
        {
                switch (strRoleName)
                {
                    case "boss":
                        return boss(strApplyUserID);
                    case "rdDispatch":
                        return rdDispatchRole();
                    case "rdAcceptTaskUser":
                        return rdAcceptTaskUser(intRequestID);
                    default:
                        return rdAcceptTaskUser(intRequestID);
                }
            }
        }

二、    Linq相關範例

l  Left Join
var query = from p1 in tcsDB.applyRequestsAuthorization
          join p2 in tcsDB.alluser on p1.userID equals p2.uid into roleUserName
          from role in roleUserName.DefaultIfEmpty()
          where p1.department == "3"
          select new flowRole
                            {
                                strRoleUserID = role.uid,
                                strRoleUserName = role.name,
                                strProcessName = "RD處理人員"
                            };

l  Union
var query = (from p1 in tcsDB.alluser
      where p1.uid == userID
      select new flowRole
      {
             strRoleUserID = p1.uid,
            strRoleUserName = p1.name,
          }).Union(
            from p2 in tcsDB.applyRequestsAuthorization
            join p3 in tcsDB.alluser on p2.userID equals p3.uid into users
            from user in users.DefaultIfEmpty()
            where p2.bossID == userID
             select new flowRole
                            {
                                strRoleUserID = user.uid,
                                strRoleUserName = user.name
                            });

三、     Angular 相關應用
本系統的簽核的畫面,將共同的畫面,獨立寫成一隻html檔,例如申請人的相關訊息(申請日期、申請理由、申請人)等等,再使用Angular Directive 呼叫共同畫面(html檔案)

l   Directive: 範例 :

angular
.directive("signViewTemplate", function () {
         return {
             templateUrl: 'signViewTemplate'
         }
     })

asp.net mvc controller
public ActionResult signViewTemplate()
        {           
                int intApplyRequestID =(int) Session["intApplyRequestID"];
                entityRequest entityRequestData = new entityRequest(); //取得實體層傳回的資料
                entityRequestData = controlObj.readApplyRequest(intApplyRequestID);
                return View(entityRequestData);
         }

檔名: SignViewTemplate.cshtml
    <div class="row">
        <div class="col-md-1">需求日期  </div>
        <div class="col-md-4">
            @Html.ValueFor(m => m.requestDate)
        </div>
    </div>
    <div class="row">
        <div class="col-md-1">申請理由</div>
        <div class="col-md-4">
            @Html.Raw(Model.applyReason)
        </div>
    </div>
<div class="row">
    <button onclick="window.history.go(-1)">返回上一頁</button>
</div>

檔名: SignViewTemplate.cshtml
@model applyRequests.Models.alluser
<br />
@using (Html.BeginForm("bossSignView", "Home", FormMethod.Post))
{
    <div class="panel panel-default" ng-controller="bossSignViewController">
        <div class="panel-heading">
            <div class="row">
                <div class="col-md-2">
                    主管簽核動作
                </div>
                <div class="col-md-4">
                    <input type="button" value="核可" onclick="location.href='@Url.Action("agree", "Home", new
                            {
                               applyRequestID = ViewData["applyRequestID"]
                             })'" />                   
                    <input type="submit" value="不准" id="strRejectReason" onclick="if (document.getElementById('rejectReasonText').value == '') { alert('請輸入不准的原因'); return false };" />
                    @Html.TextBox("rejectReasonText", null)
                    @Html.Hidden("applyRequestID", ViewData["applyRequestID"])       
                </div>
            </div>
        </div>
        <div class="panel-body">
            <div sign-view-template></div>   //呼叫directive的方式        
        </div>
    </div>
}
<script src="~/Scripts/signViewTemplate.js"></script>


l   Factory 應用

angular
  angular.module('requestApp', []).factory('myService', function ($http) {
        var listRdusers;
        return {
            getFoo: function () {
                return $http.get("../api/GetUser").then(function (result) { //呼叫Web API
                    listRdusers = result.data;
                    return listRdusers;
                });
            }
        }
    }).controller('getRDUser', function ($scope, myService) { //注入factory
        myService.getFoo().then(function (response) {           
            $scope.taskusers = response; // set value to $scope
            $scope.taskUser = response[0];
        })
    })

html
<div class="row" ng-app="requestApp">
    <div class="col-md-4" ng-controller="getRDUser">
        <select id="taskUser" ng-model="taskUser" ng-options="obj as obj.strRoleUserName for obj in taskusers"></select>
    </div>
</div>

五、     Asp.net mvc5 Session 啟用
因為安全性的問題,asp.net MVCsession 需要設定啟動模式,步驟如下所示,作者也吃了不少苦頭,不過安全起見,能避免還是改用其他方式。

Web.config
<system.webServer>
    <modules>
      <remove name="Session" />
      <add name="Session" type="System.Web.SessionState.SessionStateModule"/>
    </modules>
  </system.webServer>

Global.aspx
  protected void Session_Start()
        {
            Session["intApplyRequestID"] = "";  //初始化session
            Session["uid"] = "";
        }
        protected void Session_End()
        {     
  }

針對Session State 還有模式可運用,請參考相關資料


六、     使用google server發送gmail 信件

系統有自動發送e-mail 通知功能,但公司又沒架SMTP, 此時google的伺服器SMTP 就非常的方便,以下範例僅供參考

private static void commonSend(entityRequest entityRequestObj, string strEndMail, string strRoleUserName)
        {
            MailMessage message = new MailMessage("帳號@gmail.com", strEndMail);
            message.IsBodyHtml = true;
            message.BodyEncoding = System.Text.Encoding.UTF8;
            string applyResult = "";
            string strContext = "";         

            message.Subject = "申請人:" + entityRequestObj.applyUserName + " 提出需求日期 : " + entityRequestObj.requestDate.Value.ToString("yyyy/MM/dd")
            strContext += "申請人: " + entityRequestObj.applyUserName + "<br>";
            strContext += "申請日期: " + entityRequestObj.requestDate.Value.ToString("yyyy/MM/dd") + "<br>";
            strContext += "申請理由: " + entityRequestObj.applyReason + "<br>";
            strContext += "流程: " + entityRequestObj.processName + "<br>";
            strContext += "申請結果: " + applyResult + "<br>";
            if (entityRequestObj.predictCompleteDate != null)
            {
                strContext += "預計完工日: " + entityRequestObj.predictCompleteDate.Value.ToString("yyyy/MM/dd");
            }
            message.Body = strContext;
            var smtpClient = new SmtpClient("smtp.gmail.com", 25)
            {
                EnableSsl = true,
                DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network,
                UseDefaultCredentials = false,
                Credentials = new NetworkCredential("Gmail帳號@gmail.com", "密碼"), //設定發送gmail的帳號
            };
            smtpClient.EnableSsl = true;
            smtpClient.Send(message);
        }


    感謝大家有耐心的看完,也歡迎大家分享相關的經驗談,Angular 的語法不同JQuery,應用上些不習慣,有時混合一些JQuery 語法也許是投機取巧的方式,善用工具的利弊,對大家的專案速度和能力也有所提升